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

OnGUIをSafeArea内に収めつつ、いろんな解像度の端末で程よい大きさで表示する

SafeAreaIMGUI.cs
using UnityEngine;

public class SafeAreaIMGUI : MonoBehaviour
{
    [SerializeField]
    private float _longEdge = 1920f;

    [SerializeField]
    private float _shortEdge = 1080f;

    [SerializeField]
    private float _scale = 6;

    private void OnGUI()
    {
        var referenceWidth = Screen.orientation == ScreenOrientation.Landscape ? _longEdge : _shortEdge;
        var referenceHeight = Screen.orientation == ScreenOrientation.Portrait ? _longEdge : _shortEdge;
        var scaleFactor = _scale * Mathf.Min(Screen.width / referenceWidth, Screen.height / referenceHeight);
        BeginScaledSafeArea(scaleFactor);
        {
            _scale = GUILayout.HorizontalSlider(_scale, 6, 30f);
            GUILayout.TextArea($"scale : {_scale}", GUILayout.ExpandHeight(true), GUILayout.ExpandWidth(true));
        }
        EndScaledSafeArea();
    }

    private static void BeginScaledSafeArea(float scale)
    {
        if (Mathf.Approximately(scale, 0f)) return;
        var safeArea = Screen.safeArea;
        GUI.matrix = Matrix4x4.TRS(
            new Vector3(safeArea.xMin, Screen.height - safeArea.yMax),
            Quaternion.identity,
            new Vector3(scale, scale, 1f));
        GUILayout.BeginVertical(
            GUILayout.Height(safeArea.height / scale),
            GUILayout.Width(safeArea.width / scale));
    }

    private static void EndScaledSafeArea()
    {
        GUILayout.EndVertical();
    }
}

スケール値の計算をいい感じにやれば、 BeginScaledSafeAreaEndScaledSafeArea で囲むだけでセーフエリア内に好きなスケールでIMGUIを描画できる、かもしれない。

4倍表示
スクリーンショット 2019-11-01 午後9.15.17.png
スクリーンショット 2019-11-01 午後9.15.31.png
15倍表示
スクリーンショット 2019-11-01 午後9.16.19.png
スクリーンショット 2019-11-01 午後9.16.13.png

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

OpenCvSharpとDlibDotNetでやるFace Tracking + α

はじめに

 この記事は、↓のものの Face Tracking の部分についての解説です。
FaceRigなんてもう古い!Vをやるならこれを使え!
 Unity上でやっていますが、ほぼC#の領域です。C#の基本的なところ(C言語の基礎レベル)がわからないとたぶんわかんない。(ポインタは除く)
 僕の環境ではDlibDotNetの一部分が動かなかったのでちょこっと改造しています。(該当部分)

環境

  • Unity 2018.3.8f1
  • OpenCvSharp3 4.1.1(Nuget)
  • DlibDotNet 19.17.0.20190813(Nuget)

OpenCvSharp3・DlibDotNetの基礎

以下、ソースコードでは省略していますが、
OpenCvSharp3を使うときはusing OpenCvSharp;を、
DlibDotNetを使うときはusing DlibDotNet;
以下のようにファイルの先頭に書いてあります。

using OpenCvSharp;
using DlibDotNet;

/*  以下ソースコード
    ほげほげ        */

OpenCvSharp3 に画像を読み込む

画像を読み込むサンプルです。

Mat mat = new Mat();                                      //空のMat生成
Mat mat2 = new Mat("image.png", ImreadModes.Grayscale);   //画像読み込み

Webカメラの場合こうします。

Mat image = new Mat();                          //画像を入れる用のMat生成
VideoCapture caputure = new VideoCapture();     //Webカメラを取り扱うためのオブジェクト生成
caputure.Open(0);                               //Webカメラ起動
caputure.Read(image);                           //Webカメラの画像を読み込み

ここで出てくるMatというのは2(3)次元配列のクラスで、OpenCvの中核的なクラスで、ここに画像を入れます。細かいところまで書くと、とても長くなるのでここではあまり書きません。困ったら公式リファレンスを見てください。
※公式リファレンス : OpenCV cv::Mat Reference

また、VideoCapture.Read(Mat)で取得したMatは、VideoCapture側で管理するのが望ましく、手を加えないほうがよいです。手を加えたいときは以下のように 深い コピーをして、そちらを処理しましょう。

Mat mat_copy = image;            // × 浅いコピー
Mat mat_clone = image.Clone();   // 〇 深いコピー

※参考 : OpenCV-CookBook cv::Matの基本処理

また、Matは勝手には解放されません。このままだと動画を処理しているとメモリを使い果たすので、以下のように使い終わったら開放してやりましょう。なお、VidoCapture.Read(Mat)の引数として渡したMatは自動で管理されるので何もしないのが賢明です。(先ほどだと image

//usingパターンで自動的に破棄されるようにする。
using(Mat mat = new Mat()){
    //ほげほげ
}      

Mat mat = new Mat();
mat.Dispose();        //明示的にリソースを破棄する。

DlibDotNet に画像を読み込む

DlibDotNet 自体には直接Webカメラの画像を読み込みできないので、以下のようにして OpenCvSharp のMatを変換して使います。これは正直おまじないだと思っても大丈夫です。

    using System.Runtime.InteropServices;         //これいるよ

    Mat mat = new Mat();
    caputure.Read(mat);
    byte array = new byte[mat.Width * mat.Height * mat.ElemSize()];
    Marshal.Copy(mat.Data, array, 0, array.Length);

    Array2D<RgbPixel> image = Dlib.LoadImageData<RgbPixel>(array, (uint)mat.Height, (uint)mat.Width, (uint)(mat.Width * mat.ElemSize()));
    // imageに画像が入ってる

※参考 : DlibDotNet Wiki

Face Tracking

さて、本題のFace Trackingをする方法を紹介していきますが、その前に画像の前処理についてやっていきます。

画像の前処理

リサイズ

顔を検出するというのは非常に重いです。そこで、画像を小さくしてやることで軽くするというのがこの処理です。なお、顔検出の重さは画像の大きさの2乗(総ピクセル数)に比例します。
このリサイズの重さはほぼないと言っていいぐらいの軽さなので、安心してください。

    Mat sourse = new Mat("image.png", ImreadModes.Grayscale);  //元画像
    Mat output = new Mat();                                    //フィルター後の画像
    Cv2.Resize(sourse, output, new Size(320, 180));            //リサイズ

(第一引数, 第二引数, 第三引数)は(入力画像, 出力画像, リサイズ後の大きさ)です。
また、Matクラスは mat.Cols で横幅を、 mat.Rows で縦の高さを取得できるので、以下のようにして元画像の n分の1の画像を生成できます。

    Mat sourse = new Mat("image.png", ImreadModes.Grayscale); 
    Mat output = new Mat(); 
    int n = 2;                                                    // n分の1のn
    Cv2.Resize(sourse, output, new Size(sourse.Cols / n, sourse.Rows / n));

ガウシアンフィルタ・平均化

基本、画像にはノイズがついています。(特に動画だとノイズが大きいです。)そのノイズの影響を少なくするのがこの処理です。なんだか難しそうに聞こえますが、要はこの処理をすると顔の検出が安定するということがわかれば十分です。なお、私は軽量化のためにこの工程を入れていません。

  • 平均化       ・・・ 周りの色の平均を真ん中のピクセルの色にする。
  • ガウシアンフィルタ ・・・ 上に加えて、中央に近い色ほど重要になるように重み付けする。
    Mat sourse = new Mat("image.png", ImreadModes.Grayscale);  //元画像
    Mat output = new Mat();                                    //フィルター後の画像
    Cv2.GaussianBlur(sourse, output, new Size(5, 5), 0);       //ガウシアンフィルタ
    Cv2.Blur(sourse, output, new Size(5, 5));                  //平均化

それぞれ、(第一引数, 第二引数, 第三引数)は、(入力画像, 出力画像, 「周りの色」の広さ)です。ガウシアンフィルタの(第四引数)は、(どれだけ近さが重要か)を指定します。ここが0だと自動で入れてくれます。
※注 : 第三引数のSize()は中央のピクセルがある必要があるため、奇数しか許されていません。
※ 参考 : OpenCV Tutorials 画像の平滑化

Face Tracking

それでは本題の Face Tracking をやっていきます。やるのは顔の検出ですが、動画の毎フレームに適応すると Face Tracking となります。
なお、ここから顔の画像が入った画像が OpenCvSharp3 は Mat image 、DlibDotNet は Array2D<RgbPixel> image があるものとして使っていきます。

OpenCvSharp3 の場合

OpenCvSharp3では、カスケード分類機を作成して顔を検出させます。
なお、顔のカスケードファイルは公式が出してるのでそれを使いましょう。
OpenCVのカスケードファイル in github

    CascadeClassifier cascade = new CascadeClassifier();    // カスケード分類機作成
    cascade.Load("CASCADE_FILE_NAME.xml");                  // カスケード分類機の特徴が書かれたファイルを読み込む  

そして、作成したカスケード分類機で顔を検出します。

Rect[] faces  = cascade.DetectMultiScale(image);    // これで検出
Rect[] faces2 = cascade.DetectMultiScale(image, 1.1, 3, HaarDetectionType.FindBiggestObject, new Size(10, 10), new Size(300,300));    // こんな感じで設定もできる  

引数には、画像が入った Mat を、第二引数以降を与えてパラメーターを調整できます。
返り値は、顔が入っていると思われる長方形の 配列Rectの中身は以下の通り。

プロパティ 説明
X 長方形の左上のx座標。
元画像の左上が0で、右向き正。
Y 長方形の左上のy座標。
元画像の左上が0で、下向き正。
Width 長方形の横幅。
Height 長方形の縦幅。

実際に使うときは配列の中身があったり無かったり、複数あったりするので気を付けてください。

さらに詳しく知りたい方は、こちらの記事を見るとよいです。よくまとまっていてわかりやすかったです。
※ 参考 : 【入門者向け解説】openCV顔検出の仕組と実践(detectMultiScale)

DlibDotNet の場合

DlibDotNet では、FrontalFaceDetectorを作成して検出させます。

    FrontalFaceDetector detector = Dlib.GetFrontalFaceDetector();    // ForntalFaceDetector作成  

そして、作成したForntalFaceDetectorで顔を検出します。

    Rectangle[] rectangles  = detector.Operator(image);       // これで検出
    Rectangle[] rectangles2 = detector.Operator(image, 0);    // こんな感じで設定もできる

引数には、画像が入ったArray2D<RgbPixel>を、第二引数を与えてパラメーターを調整できます。
返り値は、顔が入っていると思われる長方形の 配列Rectangleの中身は以下の通り。

プロパティ 説明
Left 長方形の左側のx座標。
元画像の左上が0で、右向き正。
Right 長方形の右側のx座標。
元画像の左上が0で、右向き正。
Top 長方形の上側のy座標。
元画像の左上が0で、下向き正。
Bottom 長方形の下側のy座標。
元画像の左上が0で、下向き正。
Width 長方形の横幅。
Height 長方形の縦幅。

実際に使うときは OpenCvSharp3 のほうと同じように配列の中身があったり無かったり、複数あったりするので気を付けてください。

2D上の点を3D上の点に

2D上の物を3D上の物に変換する問題をPnP問題と言い、それを解くための関数が OpenCvSharp3 にあります。・・・しかし、この問題(2D→3D、位置・回転特定)を解くには最低でも6点の3D上の位置関係がわかっている点が必要です。そのために顔の特徴的な点を見つけます。なお、以下の記事がよくまとまっていて、どういう問題か把握するのに役にたちました。(問題を解くところは置いといて)
※参考 : カメラの位置・姿勢推定2 PNP問題 理論編

顔の特徴点を特定

それでは顔の特徴点を見つけていきます。そのために、ShapePredictorを作成します。
Face Landmarkのファイルは公式が配布しているのでそれを使いましょう(クリックするとダウンロードされます。)
shape pridictor 5 face landmarks
shape pridictor 68 face landmarks

    ShapePredictor shape = ShapePredictor.Deserialize("FACE_LANDMARK_FILE_NAME.dat");    // ShapePredictor 作成

作成したShapePreidictorを使って顔の特徴点を抽出します。

    // 5 landmarks だったらここも5
    DlibDotNet.Point[] points = new DlibDotNet.Point[68];
    //特徴点抽出
    using (FullObjectDetection shapes = shape.Detect(image, rectangles))
    {    // 5 landmarks だったらここも5
         for (uint i = 0; i < 68; i++)
         {  // DlibDotNet.Point という扱いやすい形に変える。
            points[i] = shapes.GetPart(i);
         }
    }

ここで注意したいのは、ただのPointだと、UnityのものとDlibDotNetのものがあり、エラーが出るということです。 DlibDotNet.Point としましょう。
Detect( , )の(第一引数, 第二引数)は、(入力画像, 顔がある領域)です。返り値は、検出点をまとめたオブジェクト FuLLObjectDetection です。
GetPart( )の(引数)は、(ほしい特徴点のインデックス)です。返り値は、指定されたインデックスの点 DlibDotNet.Point です。

上記のものがどのように検出されるかとか、他の使い方とかは以下の記事に詳しくのっているので、そちらを見てください。
※参考 : Facial landmarks with dlib
※参考 : 機械学習のライブラリ dlib

DlibDotNet を改造

私の環境では ShapePredictor.Deserialize("") がうまく動作しなかった(Dlibのdllにはなぜか渡すはずの文字列にランダムで何か追加された)ので、DlibDotNetに修正を入れてビルドしました。該当箇所は以下です。

src/DlibDotNet/ImageProcessing/ShapePredictor.cs
60 -    var str = Dlib.Encoding.GetBytes(path);                      //削除
61 -    var ret = NativeMethods.deserialize_shape_predictor(str,     //削除

60 +    var ret = NativeMethods.deserialize_shape_predictor(path,    //追加
src/DlibDotNet/PInvoke/ImageProcessing/ShapePredictor.cs
26 +    [DllImport(NativeLibrary, CallingConvention = CallingConvention)]    //追加
27 +    public static extern ErrorType deserialize_shape_predictor(string filName, out IntPtr predictor, out IntPtr errorMesage);    //追加
28 +    //追加

PnP問題を解く

PnP問題を解くのですが、私の場合、なぜか関数にMatを引数として渡してやる方法しか成功しなかったのでそれを紹介します。
まず、現実の物体の3次元の座標をセットします。私の場合、両目の内側・外側・鼻先・口の両端・あごをセットしました。

    Point3f[] model_points = new Point3f[8];                    // Matにセットする用の配列
    model_points[0] = new Point3f(0.0f, 0.03f, 0.11f);          // 鼻先
    model_points[1] = new Point3f(0.0f, -0.06f, 0.08f);         // あご
    model_points[2] = new Point3f(-0.048f, 0.07f, 0.066f);      // 右目外側
    model_points[3] = new Point3f(0.048f, 0.07f, 0.066f);       // 左目外側
    model_points[4] = new Point3f(-0.03f, -0.007f, 0.088f);     // 右くちびる
    model_points[5] = new Point3f(0.03f, -0.007f, 0.088f);      // 左くちびる
    model_points[6] = new Point3f(-0.015f, 0.07f, 0.08f);       // 右目内側
    model_points[7] = new Point3f(0.015f, 0.07f, 0.08f);        // 左目内側

    model_points_mat = new Mat(model_points.Length, 1, MatType.CV_32FC3, model_points);    // Matに変換  

そして、画像から検出された特徴点の使うやつをまとめます。

    Point2f[] image_points = new Point2f[8];                      // Matにセットする用の配列
    image_points[0] = new Point2f(points[30].X, points[30].Y);    // 鼻先
    image_points[1] = new Point2f(points[8].X, points[8].Y);      // あご
    image_points[2] = new Point2f(points[45].X, points[45].Y);    // 右目外側
    image_points[3] = new Point2f(points[36].X, points[36].Y);    // 左目外側
    image_points[4] = new Point2f(points[54].X, points[54].Y);    // 右くちびる
    image_points[5] = new Point2f(points[48].X, points[48].Y);    // 左くちびる
    image_points[6] = new Point2f(points[42].X, points[42].Y);    // 右目内側
    image_points[7] = new Point2f(points[39].X, points[39].Y);    // 左目内側

    Mat image_points_mat = new Mat(image_points.Length, 1, MatType.CV_32FC2, image_points);    // Matに変換

さらに、PnP問題を解く関数を使うための設定を作っていきます。カメラのゆがみを設定するそうですが、よくわからなかったので理想モデルを元にしています。

    Mat dist_coeffs_mat = new Mat(4, 1, MatType.CV_64FC1, 0);
    int focal_length = image.Cols;
    Point2d center = new Point2d(image.Cols / 2, image.Rows / 2);
    double[,] camera_matrix = new double[3, 3] { { focal_length, 0, center.X }, { 0, focal_length, center.Y }, { 0, 0, 1 } };
    Mat camera_matrix_mat = new Mat(3, 3, MatType.CV_64FC1, camera_matrix);

結果受け取り用の変数を作り、関数を適用させれば完成...

    Mat rvec_mat = new Mat();    // 回転ベクトル
    Mat tvec_mat = new Mat();    // 位置ベクトル
    Cv2.SolvePnP(model_points_mat, image_points_mat, camera_matrix_mat, dist_coeffs_mat, rvec_mat, tvec_mat);    // 関数適応!  

と書きたいところですが、このままじゃ使えないのでUnityで扱える形にかえてやります。

    Mat projMatrix_mat = new Mat();                   // 投影ベクトル
    double[] pos_double = double[3];                  // 位置の受け取り用
    double[] proj = double[9];                        // 回転行列受け取り用

    Marshal.Copy(tvec_mat.Data, pos_double, 0, 3);    // 位置の受け渡し
    Cv2.Rodrigues(rvec_mat, projMatrix_mat);          // 回転行列化
    Marshal.Copy(projMatrix_mat.Data, proj, 0, 9);    // 回転行列受け渡し

    Vector3 obj_position = default;                   // 最終的な位置
    Vector3 obj_rotation = default;                   // 最終的な回転

    obj_position.x = -(float)pos_double[0];           // 座標軸が違うのでこのよう
    obj_position.y = (float)pos_double[1];
    obj_position.z = (float)pos_double[2];
    obj_rotation = RotMatToQuatanion(proj).eulerAngles;  // 回転行列からクォータニオン、クォータニオンからオイラー角に変換
    // なお、RotMatToQuatanion()は自作関数で、下に畳んであります。

Quaternion RotMatToQuatanion(double[] projmat)
RotMatToQuatanion.cs
Quaternion RotMatToQuatanion(double[] projmat)
    {
        Quaternion quaternion = new Quaternion();
        double[] elem = new double[4]; // 0:x, 1:y, 2:z, 3:w
        elem[0] = projmat[0] - projmat[4] - projmat[8] + 1.0f;
        elem[1] = -projmat[0] + projmat[4] - projmat[8] + 1.0f;
        elem[2] = -projmat[0] - projmat[4] + projmat[8] + 1.0f;
        elem[3] = projmat[0] + projmat[4] + projmat[8] + 1.0f;

        uint biggestIndex = 0;
        for (uint i = 1; i < 4; i++)
        {
            if (elem[i] > elem[biggestIndex])
            {
                biggestIndex = i;
            }
        }

        if (elem[biggestIndex] < 0.0f)
        {
            return quaternion;
        }

        float v = (float)Math.Sqrt(elem[biggestIndex]) * 0.5f;
        float mult = 0.25f / v;

        switch (biggestIndex)
        {
            case 0:
                quaternion.x = v;
                quaternion.y = (float)(projmat[1] + projmat[3]) * mult;
                quaternion.z = (float)(projmat[6] + projmat[2]) * mult;
                quaternion.w = (float)(projmat[5] - projmat[7]) * mult;
                break;
            case 1:
                quaternion.x = (float)(projmat[1] + projmat[3]) * mult;
                quaternion.y = v;
                quaternion.z = (float)(projmat[5] + projmat[7]) * mult;
                quaternion.w = (float)(projmat[6] - projmat[2]) * mult;
                break;
            case 2:
                quaternion.x = (float)(projmat[6] + projmat[2]) * mult;
                quaternion.y = (float)(projmat[5] + projmat[7]) * mult;
                quaternion.z = v;
                quaternion.w = (float)(projmat[1] - projmat[3]) * mult;
                break;
            case 3:
                quaternion.x = (float)(projmat[5] - projmat[7]) * mult;
                quaternion.y = (float)(projmat[6] - projmat[2]) * mult;
                quaternion.z = (float)(projmat[1] - projmat[3]) * mult;
                quaternion.w = v;
                break;
        }

        return quaternion;
    }

なお、ここの内容はほぼここを元にしました。
※参考 : OpenCVで顔向き推定を行う
※参考 : Head Pose Estimation using OpenCV and Dlib

画像を表示したい

デバッグ用とかで、カメラ画像、もしくは加工した画像を見たいと思うことがあると思います。たとえば、正確に特徴点がとれているか調べるとか。
そこで、Cv2.Imshow(string name, Mat image)という関数があるのですが、罠です

もう一度書きます。Cv2.Imshow(string name, Mat image)は罠です。

もしそのプログラムを走らせたらフリーズして、再起動しないといけません。
ではどうするかというと、 MatTexture2Dに変換して、それを Plane などのオブジェクトに反映させてやります。

おわりに

ここまでとても長かったと思いますが、読んでくださりありがとうございます。何か助けになれたのなら幸いです。

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

FaceRigなんてもう古い!Vをやるならこれを使え!

はじめに

 この記事は宣伝用です。技術的なところは以下のリンクをたどってください。
OpenCvSharpとDlibDotNetでやるFace Tracking + α
Unityでの async・await・lock

 あと、タイトルはやや釣りぎみです。FaceRigもいいツールだと思います。私が作ったもの(kumaS.FaceTracking)も性能自体はほぼ変わらないレベルまで仕上げましたが。(タイトルはインパクトないとダメだってマッマが...)

環境

  • Windows 10 Pro
  • Unity2018.3.8f1

 Unityの設定

  • .NET 4.x Equivalent (Scripiting Runtime Version)
  • Mono (Scripiting Backend)
  • .NET Standard 2.0 (Api Compatibility Level)

 My PC のスペック

  • CPU : i7-7700HQ
  • GPU : GTX1050 (今回はあまり関係ない)
  • メモリ : 16GB

つくったものの概要(宣伝タイム)

 V界隈に仲間入りするのには高い、高い壁があった。モデルを用意するのもそうだが、何より動かすのが難しい。お金があれば他の人に任せたり、高い機器やソフトのごり押し(over 500,000 yen)できるけど、そんなにお金使えないよね?そして、こんな記事見ないと思うし。
 VR機器は高いし、手で動かしてというのはやるのは簡単だけど完成度を高めるのは難しい。(スマホで出てるやつはカスタマイズできないから除外だよ♡)
自分で作るにしてもWebカメラから動きを導くのは難しい。(Unityのfacelandmark?そこそこお金かかるよね?)ということで 私の作った「kumaS.FaceTracking」の出番だ!

  導入簡単・様々なスペックのマシンに対応・カスタマイズもしやすい・VRMとLive2D両方対応、そんなものがあるんです。
 

できること

  • Live2D・適切なボーンとBlendShapeをもった3Dモデルへのフェイストラッキングの反映
  • まばたき・目玉の位置も取得、反映
  • マシンの性能ごとに合わせたパフォーマンスの設定
  • 様々なカスタマイズに対応(ただのスクリプトなので)

デモ

 いろいろここに書くより動画見せたほうが早いので貼っときます。(変な模様がついているのはgif化した時の影響だよ)
demo2.gif

類似製品との比較

 Vを動かすものにいろいろあると思いますがそれぞれに対して(kumaS.FaceTrackingと比べた時の)メリット・デメリットをまとめました。

モーションキャプチャ用の機器

メリット
正確に、高FPSで、たくさんの箇所をキャプチャできる。性能は最強。
デメリット
高い。とにかく高い。法人か石油王じゃなきゃやってられない。

VR機器

メリット
手、頭をキャプチャできる。VRChat使えば割と楽に撮れる。
デメリット
VRChatを使わないとそこそこ難しい。そこそこお値段する。結局この世界に回帰するけど。

専用カメラ(kinectとか)

メリット
全身撮れる。
デメリット
難しい。細かいところは撮れない。安価とはいえお金かかる。

OpenCvSharpとかFacelandmarkとか

メリット
これらにできることは私のもので全てできるのでなし。
デメリット
難しい。お金かかる。

 結論。個人でやる分には私の「kumaS.FaceTracking」の勝利!

vs FaceRig

 FaceRigについて書いてなかったのはここでしっかり比較するためです。

項目 kumaS.FaceTracking FaceRig
値段 基本無料 有料
信頼性 これから(安定はしている) 世界中で使われている
FPS ぬるぬる ぬるぬる
精度 良好 良好
カスタ
マイズ
可能。パラメータはもちろん、
スクリプトも。
パラメータのみ可
対応
モデル
Live2D・ボーンとBlendShape
を設定した3Dモデル。
独自実装も可能。
独自ファイル
追加購入でLive2D可
商用 そのまま可(責任はとらない) プロ版にする必要あり

なお、FPSと精度はどちらも似たようなフェイストラッキング方法なので似たり寄ったり。

結果
私の「kumaS.FaceTracking」の勝利!
信頼性がほしい方はどうぞFaceRigをお使いください。

配布場所

 それでは!当の「kumaS.FaceTracking」ですが、Boothで販売しております。
 無料のやつと、有料のやつがあります。気を付けてください。

Face Tracking in Unity (booth)

 無料のほうは、プログラム(.unitypackage)と最低限の説明のみ。
 有料のほうは、うまく動かないときや、よりよい設定をするときのための How to 本(いや?HTMLファイル?)が入っています。
 初心者の方や、手っ取り早く使いたい方は有料のものもご購入するのをおすすめします。できるだけわかりやすく使えるようにしましたが、多機能なので慣れるまで扱いづらいかもしれません。

さいごに

 ここまで読んでくださりありがとうございました。この「kumaS.FaceTracking」で、みなさんのVライフがよりよくなれば幸いです。なお、Boothでチップを積んでくださると喜びます。

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

C#初心者の自分用メモ:最初に出てくるものの解読

Unity C#にでてくる最初の構文を解読しました

以下、サンプルコード。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class *** : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
    }

    // Update is called once per frame
    void Update()
    {
    }
}

こちらのコードを部分的に解読しました。

(注意)自分用のメモのため分かりやすさは追求していないです!!

using System.Collections; / using System.Collections.Generic; / using UnityEngine; について

「System.Collections」「System.Collections.Generic」「UnityEngine」は『名前空間』という。

名前空間とはクラスを種類ごとに整理する仕組みのこと。

ざっくり言うと、いろいろな機能があるお道具箱で、「System.Collections」「System.Collections.Generic」「UnityEngine」というお道具箱を使う(using)という意味の一文。

public class *** : MonoBehaviour について

public

他のクラスからどこからでも見えるようにする」という意味の予約語。
予約語とは「その言語内ですでに役割があるからオリジナルでは定義できない」という約束があるもの。
これが無いと、Unityエディタで使えなかったり、他のクラスから中身が見えない状態になってしまう。
逆に、隠すことを明示する場合はprivateを使う。

class ***

処理するデータとそれを処理する命令部分(メソッド)をひとつにまとめ(クラス)にするもの。
classに続く***が『クラス名』となり、このクラスを呼び出すことができる。
注意点としてUnityのルールでひとつの.csファイルで、ファイル名と同じクラスをひとつだけ宣言すると定めてあるので、それを守らないとエラーになる。

: MonoBehaviour

この後にあるクラスから派生していることを示す。
つまりクラス***は次の「MonoBehabiour」と言うクラスから派生しているよ、という意味。
MonoBehaviourは、Unityで作られるゲーム中で出てくる物体などの動作のきっかけ(イベント)で動かす処理のかたまり(メソッド)等をつなぐ役割をする。
Unityで扱うオブジェクトは、このMonoBehaviourから自動的に派生するようになっている。

void Start()とVoid Update() について

void Start()

スクリプト起動直後に1度だけ実行したい処理を書く。

void Update()

アニメーションなど「動かし続けたい」ものや、「ずっと処理し続けたい」ことを書く。
ゲームはフレーム単位ごとに更新され、その更新ごとにUpdate関数内の処理を繰り返してくれる。

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

ゲームが作りたい!!オンラインゲームの作り方全体像

2019.11執筆
初投稿です。
しきたりとか分からないので、「なんかお前やばいぞ」って思ったら教えてください。

ゲームの制作手順をおおまかに全部書いていくのですが、特に自分がオンラインゲームを作り始めるにあたって「この情報があればもっと楽に進められてた」って思うことを交えて進めていこうと思います。

「ゲーム作りたいけどそもそもどうやって作るもんなの」みたいな方の参考になればいいかな、と。

本日のメニュー

  • ゲームエンジン(ゲーム制作ツール)について
    • Unityについて
    • 実際の制作作業について
  • プログラミング言語について
  • Unityでのオンラインゲームの選択肢について
  • HTTP通信について
    • サーバーとwebアプリケーションについて
    • HTTP通信の内容について
    • GETとPOST

ゲームエンジンについて

簡単に言えばゲームはゲームエンジン(ライブラリとも言う)というソフトで作られます。
例えば有名なゲームエンジンにUnityがありますが、スマホやPCゲームの起動画面にロゴを見たことある人が多いのではないでしょうか。

例えばスマホゲームのWarRobotsやCriticalOps、最近では任天堂のスマホゲームにもUnityが使われています。
ゲームエンジンはUnityだけではなく、他にもcocos2d-xやSourceEngine、OpenGL、FoxEngine、Unreal Engine、かの有名なQuakeEngineなどがあります。

じゃあどれを使ったらいいんだこの野郎、とお思いかもしれませんがそれは個人の相性だとか、エンジンの得意分野によって決めればいいと思います。

今のところ人気があるのはUnityです。母数が多く参考書も多く、利用料がよほどの規模にならない限り無料だし、多くのプラットフォームに対応してるのが主な理由となっています。

実際どんな作業なの?

自分はUnityしか使ってませんが、ゲーム制作といってもかなりビジュアルに特化しており、直感的にオブジェクトを配置したあとプログラムで具体的にどんなことをやるかのか決める感じです。

ほかにも元から物理法則や衝突判定などゲーム制作を支援する機能がたくさんついているので、なんならそこにある機能をいじって遊んでいるだけでゲームが作れてしまいます。
興味を持ったら「Unity やり方」とかでてきとうにググってください。

プログラミング言語について

さてゲームエンジンを決めたら次はプログラミング言語を決める、と思うかもしれませんが実はエンジンによって使える言語は限られています。

つまりゲームエンジンを決めたらおのずと言語まで決まっちゃうんですよね(たいてい1か2種類)。
自分のほかの専門分野の言語と相談してエンジンを決めてもいいかもしれません。

Unityでのオンライン実装について

さぁこっからが本題です。
自分が分からなかったころの検索結果を見る限り「Unityでのオンラインゲームの作り方」はおそらく多くのユーザーが挫折しています、というかおそらく出発点にすら到達できていません(検索結果でそういう類の質問と、ぼんやりした答えを多く見てきたため)。

まぁおそらくですが一番の落とし穴は公式ドキュメントですね。
実はあのドキュメントかなり情報が未整備の状態です、というのも完全に開発者のメモみたいなものであって良い説明ではないんですよね。
ここからはそれもふまえてオンライン実装の選択肢について自分が調べた限りでお伝えします。

オンライン実装の選択肢

  • UNETを用いる方法
  • Photonを用いる方法
  • ニフティクラウドモバイルバックエンドを用いる方法
  • サーバーを用意する方法

だいたい調べた感じだとこれらが主な選択肢です。
しかし「UNETを用いる方法」は2018年に廃止され、現在は前まで使っていた開発者に対するサポート期間となっています。(入れようと思えば入れられるが公式は非推奨)

にも関わらず、公式ドキュメントの9割はこのUNETについて書かれています
これが一番の落とし穴たる要因ですね。わけもわからず「NetworkManager」の項をひたすら読み続けた方もいるのではないでしょうか(僕です)。
実質正攻法ではマルチプレイにたどり着けないっていうわけですね。

Photon、ニフティを用いる方法 

これはオンラインゲームを実装しやすくする外部パッケージを導入する方法です。

オンラインが実装しやすい分制約が多く、どうしても自由度が低くなってしまいます。

手軽に実装したい!とりあえず動けばいい!という人ならこれが正解だと思います
書籍もあり、導入方法について書かれたサイトも多いので詳しい説明はてきとうにぐぐってください。

サーバーを用いる方法

自分でサーバーサイドの処理を詳しく書いて通信をする場合です。
とはいってもそんなに難しい話ではなく、Unityでサーバーに送信したデータをサーバーで加工、保存してUnityに返すのがおおまかな流れになります。

サーバーにはサーバーソフトウェアとwebアプリケーションが必要になります。
「え?ゲームなのにwebアプリケーション?」と思うかもしれませんけどもHTTP通信を行う、という意味でwebアプリケーションが必要になります。
このwebアプリケーションが実際の処理、サーバーソフトウェアはコンピューターをインターネットにつなぐのために働きます。

つまりこの実装を行う場合はwebアプリケーションを使う知識が必要になります。
(イメージ)
IMG_0461.JPG

webアプリケーションも自分が好きなものを選べばいいと思います。
一番無難なのは母数が多いRuby on Railsです。
それでも海外ではかなり多くの開発者がいるので英語が読めればどのフレームワークでも不自由はないと思います。

以下の内容はwebアプリケーションをやってれば必ずどこかで知る基礎知識なので「なんだ、もう俺の悩みは解決したわ」っていう人はwebアプリケーションについて勉強し始めるのが良いんじゃないでしょうか。

HTTP通信の内容について

HTTP通信というとなんだか難しい気がしますが、「こういうルールで通信しようね」っていう決まりみたいなものです。
なのでルールにのっとった書き方を覚えれば難しいことはありません。

簡単に言うとHTTP通信はこのルールにのっとって書いたテキストを相手側にぶん投げるってかんじです。
つまりデータを書く作業、ぶん投げる作業、受け取って取り出す作業が必要になるわけですね。

その肝心のデータの内容は、通信の状態(ブラウザの設定とか端末情報とかどうでもいいやつ)とステータスコード(通信の成功、失敗の理由)と通信の本体(body)に分けられます。
大抵の場合このbodyに値を書いて、webアプリケーションやUnity内で取り出す形になります。

GETとPOSTについて

まぁこれもどっかで聞いたことがある方が多いのではないでしょうか。
これはクライアント(Unity)側でサーバーに通信をするときに使われる方法の種類です

  • GET データなしでサーバーにリクエスト(通信の要求)をします
  • POST データありでサーバーにリクエストをします

なんだこれ、と思いますがけっこう簡単な話です。
例えばゲームの部屋情報の一覧を得るときはデータを送る必要はありませんね。サーバー内にある部屋のデータだけ受け取ればいいので、こういう場合はGETでリクエストをします。

しかし銃を撃つ、モンスターを召喚する、アイテムを使う場合はこうはいきません。なぜなら「どんな銃をどこに向けて撃ったか」、「どんなモンスターを召喚したか」、「何のアイテムを使ったか」をサーバー側で知る必要があります。この場合はPOSTでこれらの情報を付けてリクエストする必要があります。

サーバー(webアプリケーション)内ではGETかPOSTか、どんなデータがついてるか、を判断して異なる処理をするように記述します。

終わり

まぁざっとこんな感じです。
あの時右も左も分からなった自分が読んで納得できるように書いたつもりです。

webアプリケーションは書籍がかなり多いので、そちらを流し読み(webページやフォームなど直接関係ないものが多いので)して公式ドキュメントで全体を見る感じでいいと思います。

サーバーPCについては自宅とレンタルやVPSという選択肢がありますが、それらについてはかなり分かりやすい記事があるのでそちらに任せます。
簡単に言うと判断基準はDNSの名前解決の登録料、パーツの消耗、IPの固定料金とかが絡んできます。

Unity側のGET、POSTについてはQiita内に分かりやすい記事が2つほど、スクリプトリファレンスに旧型(WWWFormを用いる方法)と新型(IMultipartFormSectionを用いる方法)について詳しく書いてあります。

VPSの利用はさくらVPSの説明ページがかなり分かりやすいのでそちらを見るのをおすすめします。

あとUnityのマニュアルはやばいので読まない方がいいと思います。とりあえずやり方をググったら具体的に出てきたクラス、関数をスクリプトリファレンスで確認する程度が普通かと。

あとはUDP、Websocketについては必要に応じてググってみてください。

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

【Unity】【第三回】

かきとちゅう

はまった点

名称未設定.png
ユニティちゃんのモデルにスクリプトをアタッチしようとしたら
「プレハブインスタンスの子要素は削除/移動できないよ」というエラーが発生。
原因は単純で、同スクリプトを2回アタッチしようとしていたため。
(ウィンドウを縮めているとアタッチ済みなことに案外気が付かない。)

参考サイト

https://xr-hub.com/archives/13700
https://unity-shoshinsha.biz/archives/935#State
https://qiita.com/r-ngtm/items/c56a2884d09deda78f22

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

【Unity】【3DAnimation】第二回 自作アニメーションを作成する。

animation用語について

リギング : スキニングやIK, FKの設定などアニメーションに必要な情報を付与する工程

スケルトン : モデルに付与する骨組み(ボーン)のこと。
       ボーンは球(関節)と線(骨)で構成される。

スキニング : ポリゴン(肉)とボーン(骨組み)の依存関係を設定する工程。

Unityにおけるアニメーションに関するファイルについて

3Dモデル : fbxファイルの一部で、メッシュ等のポリゴン情報のこと
アバター : fbxファイルの一部で、CGソフト(Blenderなど)で作成したリグとUnityで扱うリグの
       対応関係を保持する。アニメーションを行う際に必須となる。
アニメーションファイル : fbxファイルの一部で、一連のキーフレームanimationを保持する。
アニメーションコントローラファイル : 複数のアニメーションの状態遷移の管理やアニメの再生状況等を管理する。

作成してみる

参考サイト1.の通り

はまった点

Blender側でのExport時の設定がおかしかったり、フェイスアニメーション?を設定していたりすると一貫してユニティちゃんが下記ポーズをとる。
aa.png

参考サイト

1.Blenderで人型用のアニメーションを自作して、Unity上のモデルに適用するまでの流れ
2.アニメーション - Unity マニュアル
3.【Unity】「アニメーションが再生されない」「止まる」時の対処法

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

[C#]Genericの型を値型に限定したい

はじめに

C#のGenericは非常によく利用されます。
このGenericはwhereによって指定できる型をある程度制限できますが、こと値型のみに限定するということができません。

ここで言う値型とは

  • byte
  • sbyte
  • short
  • ushort
  • int
  • uint
  • long
  • ulong
  • float
  • double
  • decimal

を指します。

ただし、これらの型が共通で継承しているinterfaceをwhereで指定することでそこそこに限定できるようです。

確認環境

Unity 2019.2.0f1
IL2CPP
Android

実装

実装としてはこれだけです。
例はメソッドですがクラスでも一緒です。

void Hoge<T>() where T : struct, IComparable, IFormattable, IConvertible, IComparable<T>, IEquatable<T>
{
}

これらのinterfaceを全て実装している独自のstructなどがあればGenericに指定できてしまいますが、ほぼほぼ無いと思うのでこれで大丈夫でしょう。
実際に最初に記述した値型以外の型を指定するとコンパイルエラーになります。

参考

https://stackoverflow.com/questions/805264/how-to-define-generic-type-limit-to-primitive-types

注意

全ての型のリファレンスを確認したわけではないのでもしかしたらこれらの型以外にも一致するものがあるかもしれません。

おまけ

この制約を利用して指定した型で合計と平均を集計するクラスを実装してみました。

IL2CPPでの実装

計算を行う必要がありますが、Tのままでは計算ができないため、計算時は全てdecimalに変換しています。

AggregateIL2CPP.cs
namespace MyEngine
{
    using System;

    /// <summary>
    /// データ集計管理クラス
    /// </summary>
    public class Aggregate<T> where T : struct, IComparable, IFormattable, IConvertible, IComparable<T>, IEquatable<T>
    {
        #region Properties

        /// <summary> 合計値 </summary>
        public T Sum { get; private set; }

        /// <summary> 平均値 </summary>
        public T Average { get; private set; }

        /// <summary> 集計データのサンプル数 </summary>
        public int SampleNum { get; private set; }

        #endregion


        #region Constructor

        public Aggregate()
        {
            Clear();
        }

        #endregion


        #region Methods

        private T ConvertValue(object data)
        {
            return (T)Convert.ChangeType(data, typeof(T));
        }

        #endregion


        #region API

        /// <summary>
        /// 集計データを初期化する
        /// </summary>
        public void Clear()
        {
            SampleNum = 0;
            Sum = ConvertValue(0);
            Average = ConvertValue(0);
        }

        /// <summary>
        /// 集計データを追加
        /// </summary>
        /// <param name="data"></param>
        public void Add(T data)
        {
            SampleNum++;
            Sum = ConvertValue(Sum.ToDecimal(null) + data.ToDecimal(null));
            Average = ConvertValue(Sum.ToDecimal(null) / SampleNum);
        }

        #endregion
    }
}

Monoでの実装

Monoの場合はConvertをdynamicに置き換えることができます。

AggregateMono.cs
namespace MyEngine
{
    using System;

    /// <summary>
    /// データ集計管理クラス
    /// </summary>
    public class Aggregate<T> where T : struct, IComparable, IFormattable, IConvertible, IComparable<T>, IEquatable<T>
    {
        #region Properties

        /// <summary> 合計値 </summary>
        public T Sum { get; private set; }

        /// <summary> 平均値 </summary>
        public T Average { get; private set; }

        /// <summary> 集計データのサンプル数 </summary>
        public int SampleNum { get; private set; }

        #endregion


        #region Constructor

        public Aggregate()
        {
            Clear();
        }

        #endregion


        #region API

        /// <summary>
        /// 集計データを初期化する
        /// </summary>
        public void Clear()
        {
            SampleNum = 0;
            Sum = (dynamic)0;
            Average = (dynamic)0;
        }

        /// <summary>
        /// 集計データを追加
        /// </summary>
        /// <param name="data"></param>
        public void Add(T data)
        {
            SampleNum++;
            Sum += (dynamic)data;
            Average = (dynamic)Sum / SampleNum;
        }

        #endregion

    }
}

使用方法

適当なオブジェクトにアタッチしたら実行できます。
Aggregateのintを他の型に変えるとその型で集計するようになります。
試しにfloatにすると少数まで集計します。

PCの場合
 左クリック : 一回のみ集計
 右クリック : 毎フレーム集計

Android実機の場合
 1本指でタッチ : 一回集計
 2本指でタッチ : 毎フレーム集計
 3本指でタッチ : 集計クリア

AggregateTest.cs
using UnityEngine;
using MyEngine;

using Random = System.Random;

public class AggregateTest : MonoBehaviour
{
    Aggregate<int> _aggregate = new Aggregate<int>();

    // Start is called before the first frame update
    void Start()
    {
    }

    // Update is called once per frame
    void Update()
    {
        bool isUpdate = false;

#if UNITY_EDITOR
        if (Input.GetMouseButtonDown(0) || Input.GetMouseButton(1))
        {
            isUpdate = true;
        }
#else

        if (Input.touchCount == 1 && Input.touches[0].phase == TouchPhase.Began)
        {
            isUpdate = true;
        }
        else if (Input.touchCount == 2)
        {
            isUpdate = true;
        }
        else if (Input.touchCount == 3)
        {
            _aggregate.Clear();
        }
#endif

        if (isUpdate)
        {
            Random rand = new Random();
            _aggregate.Add((int)(rand.NextDouble() * 100));
        }
    }

    private void OnGUI()
    {
        GUILayout.Label($"合計={_aggregate.Sum:N}\n平均={_aggregate.Average:N}\nサンプル数={_aggregate.SampleNum:N}");
    }
}

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

【C#】Genericの型を値型に限定したい

はじめに

C#のGenericは非常によく利用されます。
このGenericはwhereによって指定できる型をある程度制限できますが、こと値型のみに限定するということができません。

ここで言う値型とは

  • byte
  • sbyte
  • short
  • ushort
  • int
  • uint
  • long
  • ulong
  • float
  • double
  • decimal

を指します。

ただし、これらの型が共通で継承しているinterfaceをwhereで指定することでそこそこに限定できるようです。

確認環境

Unity 2019.2.0f1
IL2CPP
Android

実装

実装としてはこれだけです。
例はメソッドですがクラスでも一緒です。

void Hoge<T>() where T : struct, IComparable, IFormattable, IConvertible, IComparable<T>, IEquatable<T>
{
}

これらのinterfaceを全て実装している独自のstructなどがあればGenericに指定できてしまいますが、ほぼほぼ無いと思うのでこれで大丈夫でしょう。
実際に最初に記述した値型以外の型を指定するとコンパイルエラーになります。

参考

https://stackoverflow.com/questions/805264/how-to-define-generic-type-limit-to-primitive-types

注意

全ての型のリファレンスを確認したわけではないのでもしかしたらこれらの型以外にも一致するものがあるかもしれません。

おまけ

この制約を利用して指定した型で合計と平均を集計するクラスを実装してみました。

IL2CPPでの実装

計算を行う必要がありますが、Tのままでは計算ができないため、計算時は全てdecimalに変換しています。

AggregateIL2CPP.cs
namespace MyEngine
{
    using System;

    /// <summary>
    /// データ集計管理クラス
    /// </summary>
    public class Aggregate<T> where T : struct, IComparable, IFormattable, IConvertible, IComparable<T>, IEquatable<T>
    {
        #region Properties

        /// <summary> 合計値 </summary>
        public T Sum { get; private set; }

        /// <summary> 平均値 </summary>
        public T Average { get; private set; }

        /// <summary> 集計データのサンプル数 </summary>
        public int SampleNum { get; private set; }

        #endregion


        #region Constructor

        public Aggregate()
        {
            Clear();
        }

        #endregion


        #region Methods

        private T ConvertValue(object data)
        {
            return (T)Convert.ChangeType(data, typeof(T));
        }

        #endregion


        #region API

        /// <summary>
        /// 集計データを初期化する
        /// </summary>
        public void Clear()
        {
            SampleNum = 0;
            Sum = ConvertValue(0);
            Average = ConvertValue(0);
        }

        /// <summary>
        /// 集計データを追加
        /// </summary>
        /// <param name="data"></param>
        public void Add(T data)
        {
            SampleNum++;
            Sum = ConvertValue(Sum.ToDecimal(null) + data.ToDecimal(null));
            Average = ConvertValue(Sum.ToDecimal(null) / SampleNum);
        }

        #endregion
    }
}

Monoでの実装

Monoの場合はConvertをdynamicに置き換えることができます。

AggregateMono.cs
namespace MyEngine
{
    using System;

    /// <summary>
    /// データ集計管理クラス
    /// </summary>
    public class Aggregate<T> where T : struct, IComparable, IFormattable, IConvertible, IComparable<T>, IEquatable<T>
    {
        #region Properties

        /// <summary> 合計値 </summary>
        public T Sum { get; private set; }

        /// <summary> 平均値 </summary>
        public T Average { get; private set; }

        /// <summary> 集計データのサンプル数 </summary>
        public int SampleNum { get; private set; }

        #endregion


        #region Constructor

        public Aggregate()
        {
            Clear();
        }

        #endregion


        #region API

        /// <summary>
        /// 集計データを初期化する
        /// </summary>
        public void Clear()
        {
            SampleNum = 0;
            Sum = (dynamic)0;
            Average = (dynamic)0;
        }

        /// <summary>
        /// 集計データを追加
        /// </summary>
        /// <param name="data"></param>
        public void Add(T data)
        {
            SampleNum++;
            Sum += (dynamic)data;
            Average = (dynamic)Sum / SampleNum;
        }

        #endregion

    }
}

使用方法

適当なオブジェクトにアタッチしたら実行できます。
Aggregateのintを他の型に変えるとその型で集計するようになります。
試しにfloatにすると少数まで集計します。

PCの場合
 左クリック : 一回のみ集計
 右クリック : 毎フレーム集計

Android実機の場合
 1本指でタッチ : 一回集計
 2本指でタッチ : 毎フレーム集計
 3本指でタッチ : 集計クリア

AggregateTest.cs
using UnityEngine;
using MyEngine;

using Random = System.Random;

public class AggregateTest : MonoBehaviour
{
    Aggregate<int> _aggregate = new Aggregate<int>();

    // Start is called before the first frame update
    void Start()
    {
    }

    // Update is called once per frame
    void Update()
    {
        bool isUpdate = false;

#if UNITY_EDITOR
        if (Input.GetMouseButtonDown(0) || Input.GetMouseButton(1))
        {
            isUpdate = true;
        }
#else

        if (Input.touchCount == 1 && Input.touches[0].phase == TouchPhase.Began)
        {
            isUpdate = true;
        }
        else if (Input.touchCount == 2)
        {
            isUpdate = true;
        }
        else if (Input.touchCount == 3)
        {
            _aggregate.Clear();
        }
#endif

        if (isUpdate)
        {
            Random rand = new Random();
            _aggregate.Add((int)(rand.NextDouble() * 100));
        }
    }

    private void OnGUI()
    {
        GUILayout.Label($"合計={_aggregate.Sum:N}\n平均={_aggregate.Average:N}\nサンプル数={_aggregate.SampleNum:N}");
    }
}

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

【Unity】uGUIのドロップダウンメニューをスクリプトから作る

myGIf.gif

Main.cs
static Dropdown GenDropdown(RectTransform parent)
    {
        Dropdown dropdown = new GameObject("dropdown").AddComponent<Dropdown>();
        RectTransform dropdownRC = dropdown.GetComponent<RectTransform>();
        dropdownRC.SetParent(parent);
        Util.SetAnchorPointZero(dropdownRC);

        dropdown.captionText = Util.AddTextRight(dropdownRC); // 選択中のテキストが選択される     
        {
            // dropdown.targetGraphicを設定してない場合、dropdown.captionTextの領域を開けばドロップダウンメニューが開く
            dropdown.targetGraphic = dropdown.gameObject.AddComponent<Image>(); // ドロップダウンメニューを開くための画像領域
            dropdown.targetGraphic.color = Color.green;
        }        

        RectTransform template = new GameObject("template").AddComponent<RectTransform>(); // 一覧表示される際のひな型(テンプレート)
        template.gameObject.SetActive(false);
        template.SetParent(dropdownRC);
        Util.SetAnchorPointZero(template);
        Toggle tog = Util.GenToggle(template, dropdownRC.sizeDelta.x);
        RectTransform togRC = tog.GetComponent<RectTransform>();
        togRC.localPosition = new Vector2(0, -dropdownRC.sizeDelta.y);
        Text text = Util.AddTextRight(togRC); // 一覧の表示されるテキスト

        dropdown.template = template;
        dropdown.itemText = text;

        return dropdown;
    }

    void Test()
    {
        Dropdown dropdown = GenDropdown(GameObject.Find("Canvas").GetComponent<RectTransform>());
        dropdown.options.Add(new Dropdown.OptionData { text = "AAA" });
        dropdown.options.Add(new Dropdown.OptionData { text = "BBB" });
        dropdown.options.Add(new Dropdown.OptionData { text = "CCC" });
        dropdown.onValueChanged.AddListener((data) => MyOnValueChanged(data));
    }

    static public void MyOnValueChanged(int listNum)
    {
        Debug.Log(listNum); // BBBを選択した場合、1が返される。dropdown.optionsのリストのインデックスが返される。
    }

    void Start()
    {
        Test();
    }
Util.cs
static public RectTransform SetAnchorPointZero(RectTransform rect)
    {
        // アンカーポイントを左上に合わせ、ローカル座標をリセットする。
        rect.anchorMin = Vector2.up;
        rect.anchorMax = Vector2.up;
        rect.pivot = Vector2.up;
        rect.localPosition = Vector3.zero;
        rect.rotation = Quaternion.identity;
        rect.localScale = Vector3.one;
        return rect;
    }

    static public Toggle GenToggle(RectTransform parent, float checkBoxSize = 50)
    {
        Toggle tog = new GameObject("togle").AddComponent<Toggle>();
        tog.isOn = true;
        Image background = new GameObject("background").AddComponent<Image>();
        Image checkmark = new GameObject("checkmark").AddComponent<Image>();

        RectTransform togRC = tog.GetComponent<RectTransform>();
        checkmark.rectTransform.SetParent(background.rectTransform);
        background.rectTransform.SetParent(togRC);

        togRC.sizeDelta = Vector2.one * checkBoxSize;
        background.rectTransform.sizeDelta = Vector2.one * checkBoxSize;
        checkmark.rectTransform.sizeDelta = Vector2.one * checkBoxSize * 0.8f;
        checkmark.color = Color.red;

        tog.graphic = checkmark;
        tog.targetGraphic = background;

        togRC.SetParent(parent);
        SetAnchorPointZero(togRC);

        return tog;
    }

    // あるRectTransformの右側に文字を付け加える
    static public Text AddTextRight(RectTransform rc, float offsetX_scale = 1.2f)
    {        
        Text text = new GameObject("text").AddComponent<Text>();
        text.font = Resources.Load<Font>("Font/mplus-1c-black");
        if (text.font == null)
            throw new Exception("フォントが見つかりません!");        
        text.fontSize = (int)(rc.sizeDelta.y * 0.7f); // 0.7倍までなら文字が描画される。
        text.horizontalOverflow = HorizontalWrapMode.Overflow;
        text.rectTransform.sizeDelta = new Vector2(100, rc.sizeDelta.y);
        text.rectTransform.SetParent(rc);
        SetAnchorPointZero(text.rectTransform).localPosition = new Vector2(rc.sizeDelta.x * offsetX_scale, 0);
        return text;
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【UnityEditor】テクスチャ閲覧ツールを作ってみた

はじめに

プロジェクト内にテクスチャが増えてくると、インポート設定やデータサイズに問題が無いかのチェックを行うのが大変になってきます。
そこでプロジェクト内に存在するテクスチャを一か所でまとめて見れるようなツールを作成してみたところ、とても便利だったのでご紹介したいと思います。

作ったもの

テクスチャの情報を1か所にまとめてみることができます。
image.png

GitHubにて公開中です(MITライセンス)
https://github.com/rngtm/UnityEditor-TextureViewer

ソートできる

大きすぎるファイルや設定に漏れがあるファイルを一目で見つけることができます。
1b.gif

環境

Unity2018.4.3f1

ツールの実装について

今回はTreeViewを利用してツールを実装しました。

TreeViewの参考リンク

TreeViewの実装については以下のブログを参考にさせていただきました。
http://light11.hatenadiary.com/entry/2019/02/07/010146

Unity公式リンク
https://docs.unity3d.com/Manual/TreeViewAPI.html

TreeViewのソースコード(UnityCsReference)
https://github.com/Unity-Technologies/UnityCsReference/tree/master/Editor/Mono/GUI/TreeView

実装 解説

実装を全て載せると長くなってしまうので、要所をかい摘んで解説いたします。

【Tips 1】データの定義

今回は以下の情報を表示させるようにしてみました。
・ テクスチャについての情報(テクスチャの名前や解像度など)
・ テクスチャインポート設定

上記を表現するクラスを以下のように定義しました。

TextureTreeElement.cs
/** ********************************************************************************
 * @summary TreeViewで表示するデータ
 ***********************************************************************************/
public class TextureTreeElement
{
    private ulong textureByteLength = 0; // テクスチャファイルのバイト長
    private string textureDataSizeText = ""; // textureByteLengthを読みやすくテキストで表現したもの
    public string AssetPath { get; set; } // 背景アセットパス
    public string AssetName { get; set; } // 背景アセット名
    public ulong TextureByteLength => textureByteLength; // テクスチャデータサイズ(Byte)
    public string TextureDataSizeText => textureDataSizeText;// テクスチャデータサイズテキスト
    public Texture2D Texture { get; set; } // ロードしたテクスチャ 
    public TextureImporter TextureImporter { get; set; } // テクスチャインポート設定
    public int Index { get; set; } // 何番目の要素か(TreeViewで表示する際に使う)
    public TextureTreeElement Parent { get; private set; } // 親の要素(TreeViewで表示する際に使う)
    public List<TextureTreeElement> Children { get; } = new List<TextureTreeElement>(); // 子の要素(TreeViewで表示する際に使う)

    /** ********************************************************************************
    * @summary データサイズ更新
    ***********************************************************************************/
    public void UpdateDataSize()
    {
        textureByteLength = (ulong)Texture.GetRawTextureData().Length;
        textureDataSizeText = UIUtils.ConvertToHumanReadableSize(textureByteLength);
    }

    /** ********************************************************************************
    * @summary 子を追加
    ***********************************************************************************/
    internal void AddChild(TextureTreeElement child)
    {
        // 既に親がいたら削除
        if (child.Parent != null)
        {
            child.Parent.RemoveChild(child);
        }

        // 親子関係を設定
        Children.Add(child);
        child.Parent = this;
    }

    /** ********************************************************************************
    * @summary 子を削除
    ***********************************************************************************/
    public void RemoveChild(TextureTreeElement child)
    {
        if (Children.Contains(child))
        {
            Children.Remove(child);
            child.Parent = null;
        }
    }
}

【Tips 2】表の見た目のTreeViewを作る

MultiColumnHeaderを利用するコンストラクタを使うことで、表のような見た目のTreeViewが作れるようになります。

public partial class TextureTreeView : TreeView
{
    /** ********************************************************************************
    * @summary コンストラクタ
    ***********************************************************************************/
    public TextureTreeView(TreeViewState state) : base(new TreeViewState(), new TextureColumnHeader(new MultiColumnHeaderState(headerColumns)))
    {
        showAlternatingRowBackgrounds = true; // 背景のシマシマを表示
        showBorder = true; // 境界線を表示

        multiColumnHeader.sortingChanged += OnSortingChanged; // ソート変化時の処理を登録
    }

【Tips 3】ヘッダーの実装

image.png

列を定義する

ヘッダー列を定義するには MultiColumnHeaderStateMultiColumnHeaderState.Column[] を渡してやります。

TextureTreeView.cs
    // TreeViewのヘッダーの列の定義
    static readonly TextureColumn[] headerColumns = new[] {
        new TextureColumn("Texture", 170f), // 0
        new TextureColumn("Texture Type", 105f), // 1
        new TextureColumn("Non Power of 2", 105f), // 2
        new TextureColumn("Max Size", 70f), // 3
        new TextureColumn("Generate\nMip Maps", 70f), // 4
        new TextureColumn("Alpha is\nTransparency", 96f), // 5
        new TextureColumn("Texture Size", 105f), // 6
        new TextureColumn("Data Size", 80f), // 7
    };

    /** ********************************************************************************
    * @summary コンストラクタ
    ***********************************************************************************/
    public TextureTreeView(TreeViewState state) 
    : base(new TreeViewState(), new TextureColumnHeader(new MultiColumnHeaderState(headerColumns)))
    {

...(以下省略)



MultiColumnHeaderState のコンストラクタ引数には MultiColumnHeaderState.Column の派生クラス を渡しています。

TextureColumn.cs
/** ********************************************************************************
* @summary MultiColumnHeaderState.Columnの派生クラス
***********************************************************************************/
public class TextureColumn : MultiColumnHeaderState.Column
{
    /** ********************************************************************************
    * @summary コンストラクタ
    * @param   label : 列のヘッダー文字
    * @param   width : 列の横幅(pixel)
    ***********************************************************************************/
    public TextureColumn(string label, float width) : base()
    {
        base.width = width;
        autoResize = false; // 横幅が勝手に変わらないようにする
        headerContent = new GUIContent(label); 
    }
}

ヘッダーの大きさを変える

ヘッダーの大きさを変えたい場合はMultiColumnHeaderheight に数値を代入します。

今回は派生クラスを作成し、コンストラクタにてheaderに数値を代入しました。

TextureColumnHeader.cs
/** ********************************************************************************
* @summary MultiColumnHeaderの派生クラス
***********************************************************************************/
public class TextureColumnHeader : MultiColumnHeader
{
    public static readonly float headerHeight = 36f;

    /** ********************************************************************************
    * @summary コンストラクタ
    ***********************************************************************************/
    public TextureColumnHeader(MultiColumnHeaderState state) : base(state)
    {
        height = headerHeight; // ヘッダーの高さ 上書き
    }

ヘッダーの文字を下ぞろえにする

MultiColumnHeader クラスの ColumnHeaderGUI() メソッドをオーバーライドし、ラベルを下ぞろえにしてみました。

実装にはUnityCsReferenceにあるMultiColumnHeader.csを参考にしました。

TextureColumnHeader.cs
     static readonly float labelY = 4f; // ラベル位置
     private GUIStyle style;

     /** ********************************************************************************
     * @summary TreeViewのヘッダー描画
     ***********************************************************************************/
     protected override void ColumnHeaderGUI(MultiColumnHeaderState.Column column, Rect headerRect, int columnIndex)
     {
         if (canSort && column.canSort)
         {
             SortingButton(column, headerRect, columnIndex);
         }

         if (style == null)
         {
             style = new GUIStyle(DefaultStyles.columnHeader);
             style.alignment = TextAnchor.LowerLeft; // 下ぞろえにする
         }

         float labelHeight = headerHeight;
         Rect labelRect = new Rect(headerRect.x, headerRect.yMax - labelHeight - labelY, headerRect.width, labelHeight);
         GUI.Label(labelRect, column.headerContent, style);
     }

【Tips 4】BuildRootメソッドの実装

TreeView を実装するには BuildRootBuildRows メソッドを実装する必要があります。

TreeView.BuildRoot() ではTreeViewの最も根本に位置するアイテムを作成する処理を実装します。
ここで作成するルートアイテムはTreeView上では表示されません。

TextureTreeView.cs
/** ********************************************************************************
* @summary ルートとなる要素を作成
***********************************************************************************/
protected override TreeViewItem BuildRoot()
{
    // BuildRootではRootだけを作成して返す
    return new TextureTreeViewItem { id = -1, depth = -1, displayName = "Root" };
}

【Tips 5】BuildRowsメソッドの実装

TreeView.BuildRows() では表の列を作成する処理を実装します。

TextureTreeView.cs
private static readonly TreeViewItem DummyTreeViewItem 
    = new TreeViewItem { id = -999, depth = 0, displayName = "なし" };
private static readonly List<TreeViewItem> DummyTreeViewList 
    = new List<TreeViewItem> { DummyTreeViewItem };

 /** ********************************************************************************
 * @summary 列の作成
 * @note    BuildRows()で返されたIListを元にしてTreeView上で描画が実行されます。
 ***********************************************************************************/
 protected override IList<TreeViewItem> BuildRows(TreeViewItem root)
 {
     var rows = base.BuildRows(root); // TreeView.BuildRows()の内部では検索による絞り込みを行っている
     if (hasSearch && rows.Count == 0) // 検索ヒットなしの場合
     {
         return DummyTreeViewList; // ダミーのデータを返しておく(リストの要素にnullが存在する場合、エラーが発生するので注意)
     }

     // TreeViewItemの親子関係を構築
     var elements = new List<TreeViewItem>();

     CustomUI.RowCount = baseElements.Count();
     foreach (var baseElement in baseElements)
     {
         var baseItem = CreateTreeViewItem(baseElement) as TextureTreeViewItem;
         baseItem.data = baseElement; // ソートに利用するデータを設定

         root.AddChild(baseItem); // ルートに追加
         rows.Add(baseItem); // 列に追加
     }

     // 親子関係に基づいてDepthを自動設定するメソッド
     SetupDepthsFromParentsAndChildren(root);

     return rows;
 }
TextureTreeViewItem.cs
/** ********************************************************************************
* @summary TreeViewItemの派生クラス
***********************************************************************************/
public class TextureTreeViewItem : TreeViewItem
{
    public TextureTreeElement data { get; set; }
}

補足: GetRows()を使うと検索が無効になる

BuildRows()の中で列データを取得する際に TreeView.GetRows() を使うと検索による絞り込みが無効になってしまうので注意してください。

TreeView.BuildRows() を使うと検索の結果を得ることができます。

TextureTreeView.cs
var rows = base.BuildRows(root); // TreeView.BuildRows()の内部では検索による絞り込みを行っている



ちなみに、TreeView.BuildRows() の中身の実装はUnityCsReferenceのTreeViewControl.csから見ることができます。

TreeViewControl.cs
    // Default implementation of BuildRows assumes full tree was built in BuildRoot. With full tree we can also support search out of the box
    protected virtual IList<TreeViewItem> BuildRows(TreeViewItem root)
    {
        // Reuse cached list (for capacity)
        if (m_DefaultRows == null)
            m_DefaultRows = new List<TreeViewItem>(100);
        m_DefaultRows.Clear()      

        if (hasSearch)
            m_DataSource.SearchFullTree(searchString, m_DefaultRows);
        else
            AddExpandedRows(root, m_DefaultRows);
        return m_DefaultRows;
    }

【Tips 6】全ての行の描画処理

RowGUIのオーバーライドを実装することで、TreeViewの描画処理を実装することができます。

TextureTreeView.cs
private Texture2D iconTexture = null; // Prefabアイコン

 /** ********************************************************************************
  * @summary TreeViewの列の描画
  ***********************************************************************************/
 protected override void RowGUI(RowGUIArgs args)
 {
     if (iconTexture == null)
     {
         // Prefabアイコンをロード
         iconTexture = EditorGUIUtility.Load("Prefab Icon") as Texture2D;
     }

     // TreeViewの各列の描画
     for (var visibleColumnIndex = 0; visibleColumnIndex < args.GetNumVisibleColumns(); visibleColumnIndex++)
     {
         var rect = args.GetCellRect(visibleColumnIndex); // 描画範囲を取得
         var columnIndex = args.GetColumn(visibleColumnIndex); // 列のインデックス取得(最左列の場合は0を取得)
         var labelStyle = args.selected ? EditorStyles.whiteLabel : EditorStyles.label;
         labelStyle.alignment = fieldLabelAnchor; // テキストを左揃えにする

         DrawRowColumn(args, rect, columnIndex); // 列を描画
     }
 }

【Tips 7】行1つの描画処理

RowGUIArgs.GetColumn() メソッドを利用することで、今描画しようとしている列が何番目かを知ることができます。
今回はswitch文で分岐させ、対応する描画処理を記述しました。

テキスト表示には EditorGUI.LabelField() を利用しています。

TextureTreeView.cs
private const int MB = 1024 * 1024; // メガバイト
private const int yellowDataSize = 2 * MB; // データサイズがこれを超えたら黄色で警告
private const int redDataSize = 3 * MB; // データサイズがこれを超えたら赤で警告
private const int yellowTextureSize = 2048; // テクスチャサイズがこれを超えたら黄色で警告
private const int redTextureSize = 4096; // テクスチャサイズがこれを超えたら黄色で警告
private const int redMaxTextureSize = 2048; // テクスチャ最大サイズがこれを超えたら赤で警告

/** ********************************************************************************
* @summary 列の行を描画
***********************************************************************************/
private void DrawRowColumn(RowGUIArgs args, Rect rect, int columnIndex)
{
    if (args.item.id < 0) { return; }  // 検索がヒットしない場合はid=-999のダミー(DummyTreeViewItem)が入ってくる。ここでは描画をスキップする

    TextureTreeElement element = baseElements[args.item.id];

    var texture = element.Texture;
    if (element.Texture == null) { return; }
    if (element.TextureImporter == null) { return; }

    GUIStyle labelStyle = EditorStyles.label;

    switch (columnIndex)
    {
        case (int)EHeaderColumn.TextureName:
            rect.x += 2f;

            // アイコンを描画する
            Rect toggleRect = rect;
            toggleRect.y += 2f;
            toggleRect.size = new Vector2(12f, 12f);
            GUI.DrawTexture(toggleRect, texture);

            // テキストを描画する
            Rect labelRect = new Rect(rect);
            labelRect.x += toggleRect.width;
            EditorGUI.LabelField(labelRect, args.label);
            break;
        case (int)EHeaderColumn.TextureType: // TextureType            
            EditorGUI.LabelField(rect, element.TextureImporter.textureType.ToString()); 
            break;
        case (int)EHeaderColumn.NPot: // Non power of 2
            if (element.TextureImporter.npotScale == TextureImporterNPOTScale.None)
            {
                labelStyle = MyStyle.RedLabel;
            }
            EditorGUI.LabelField(rect, element.TextureImporter.npotScale.ToString(), labelStyle);
            break;
        case (int)EHeaderColumn.MaxSize: // Max size
            if (element.TextureImporter.maxTextureSize > redMaxTextureSize)
            {
                labelStyle = MyStyle.RedLabel;
            }
            EditorGUI.LabelField(rect, element.TextureImporter.maxTextureSize.ToString(), labelStyle);
            break;
        case (int)EHeaderColumn.GenerateMips: // Generate mip maps
            if (element.TextureImporter.mipmapEnabled == true)
            {
                labelStyle = MyStyle.RedLabel;
            }
            EditorGUI.LabelField(rect, element.TextureImporter.mipmapEnabled.ToString(), labelStyle);
            break;
        case (int)EHeaderColumn.AlphaIsTransparency: // Alpha is Transparency
            EditorGUI.LabelField(rect, element.TextureImporter.alphaIsTransparency.ToString());
            break;
        case (int)EHeaderColumn.TextureSize: // Texture Size
            switch ((element.Texture.width, element.Texture.height))
            {
                case var values when values.width > redTextureSize || values.height > redTextureSize:
                    labelStyle = MyStyle.RedLabel;
                    break;
                case var values when values.width > yellowTextureSize || values.height > yellowTextureSize:
                    labelStyle = MyStyle.YellowLabel;
                    break;
            }
            EditorGUI.LabelField(rect, $"{element.Texture.width}x{element.Texture.height}", labelStyle);
            break;
        case (int)EHeaderColumn.DataSize: // データサイズ
            switch ((int)element.TextureByteLength)
            {
                case int len when len > redDataSize:
                    labelStyle = MyStyle.RedLabel;
                    break;
                case int len when len > yellowDataSize:
                    labelStyle = MyStyle.YellowLabel;
                    break;
                default:
                    break;
            }
            EditorGUI.LabelField(rect, element.TextureDataSizeText, labelStyle);
            break;
    }
}
MyStyle.cs
using UnityEngine;
using UnityEditor;

/** ********************************************************************************
* @summary GUIStyleなどの定義
***********************************************************************************/
public static class MyStyle
{
    public static GUIStyle YellowLabel { get; private set; } // 黄色いラベル
    public static GUIStyle RedLabel { get; private set; } // 赤いラベル

    /** ********************************************************************************
    * @summary GUIStyleが無ければ作成
    ***********************************************************************************/
    public static void CreateGUIStyleIfNull()
    {
        if (YellowLabel == null)
        {
            YellowLabel = new GUIStyle(EditorStyles.label);
            YellowLabel.normal.textColor = Color.yellow;
        }

        if (RedLabel == null)
        {
            RedLabel = new GUIStyle(EditorStyles.label);
            RedLabel.normal.textColor = new Color(1f, 0.1f, 0f);
        }
    }
}

【Tips 8】検索の実装

TreeView.searchStringに検索文字列を設定すると勝手に絞り込んでくれます。

検索絞り込みの実装
treeView.searchString = searchField.OnToolbarGUI(treeView.searchString, GUILayout.MaxWidth(280f));

今回は検索文字列が変化したときにだけTreeView.searchStringに代入するようにしてみました。

TextureViewerWindow.cs
EditorGUI.BeginChangeCheck();
searchText = searchField?.OnToolbarGUI(searchText, GUILayout.MaxWidth(280f));
if (EditorGUI.EndChangeCheck())
{
    if (treeView != null)
    {
        // TreeView.searchStringに検索文字列を入れると表示Itemを絞ってくれる
        treeView.searchString = searchText;
    }
}

【Tips 9】ソートの実装

ソートに関しての実装は複雑なので、今回はソースコードだけ載せます。

multiColumnHeader.sortingChangedにソート変化メソッドを登録

multiColumnHeader.sortingChanged にメソッドを登録することで、ヘッダーをクリックした際にメソッドが実行されるようになります。

image.png

TextureTreeView.cs
        /** ********************************************************************************
        * @summary コンストラクタ
        ***********************************************************************************/
        public TextureTreeView(TreeViewState state)
            //: base(state, new TextureColumnHeader(new MultiColumnHeaderState(headerColumns)))
        : base(new TreeViewState(), new TextureColumnHeader(new MultiColumnHeaderState(headerColumns)))
        {
            showAlternatingRowBackgrounds = true; // 背景のシマシマを表示
            showBorder = true; // 境界線を表示

            multiColumnHeader.sortingChanged += OnSortingChanged; // ソート変化時の処理を登録
        }

TreeViewのサンプルを参考にソート処理を実装してみました。

公式リファレンスのページにあるTreeViewExamples.zip がサンプルデータになります。

TextureTreeView_Sort.cs
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor.IMGUI.Controls;

public partial class TextureTreeView : TreeView
{
    // 列に対応するソート
    EHeaderColumn[] m_SortOptions =
    {
        EHeaderColumn.TextureName,
        EHeaderColumn.TextureType,
        EHeaderColumn.NPot,
        EHeaderColumn.MaxSize,
        EHeaderColumn.GenerateMips,
        EHeaderColumn.AlphaIsTransparency,
        EHeaderColumn.TextureSize,
        EHeaderColumn.DataSize,
    };

    // ソートに使用するデータの選択
    static readonly Func<TextureTreeElement, object>[] sortSelectors = new Func<TextureTreeElement, object>[]
    {
        l => l.AssetName,
        l => l.TextureImporter.textureType,
        l => l.TextureImporter.npotScale, // Non power of two
        l => l.TextureImporter.maxTextureSize, // max size
        l => l.TextureImporter.mipmapEnabled, // generate mip maps
        l => l.TextureImporter.alphaIsTransparency,
        l => l.Texture.width* l.Texture.width, // Texture Size
        l => l.TextureByteLength, // Data Size
    };

    ///** ********************************************************************************
    //* @summary Treeをリスト形式にする
    //***********************************************************************************/
    public static void TreeToList(TreeViewItem root, IList<TreeViewItem> result)
    {
        if (root == null)
            throw new NullReferenceException("root");
        if (result == null)
            throw new NullReferenceException("result");

        result.Clear();

        if (root.children == null)
            return;

        Stack<TreeViewItem> stack = new Stack<TreeViewItem>();
        for (int i = root.children.Count - 1; i >= 0; i--)
            stack.Push(root.children[i]);

        while (stack.Count > 0)
        {
            TreeViewItem current = stack.Pop();
            result.Add(current);

            if (current.hasChildren && current.children[0] != null)
            {
                for (int i = current.children.Count - 1; i >= 0; i--)
                {
                    stack.Push(current.children[i]);
                }
            }
        }
    }

    ///** ********************************************************************************
    //* @summary ソート状態の変化時に呼ばれる
    //***********************************************************************************/
    void OnSortingChanged(MultiColumnHeader multiColumnHeader)
    {
        SortIfNeeded(rootItem, GetRows());
    }

    ///** ********************************************************************************
    //* @summary ソート実行
    //***********************************************************************************/
    void SortIfNeeded(TreeViewItem root, IList<TreeViewItem> rows)
    {
        if (rows.Count <= 1)
            return;

        if (multiColumnHeader.sortedColumnIndex == -1)
        {
            return; // No column to sort for (just use the order the data are in)
        }

        // Sort the roots of the existing tree items
        SortByMultipleColumns();
        TreeToList(root, rows);
        Repaint();
    }

    ///** ********************************************************************************
    //* @summary ソート実行
    //***********************************************************************************/
    void SortByMultipleColumns()
    {
        var sortedColumns = multiColumnHeader.state.sortedColumns;

        if (sortedColumns.Length == 0)
            return;

ar myTypes = rootItem.children.Cast<TextureTreeViewItem>();
        var orderedQuery = InitialOrder(myTypes, sortedColumns);
        for (int i = 1; i < sortedColumns.Length; i++)
        {
            EHeaderColumn sortOption = m_SortOptions[sortedColumns[i]];
            bool ascending = multiColumnHeader.IsSortedAscending(sortedColumns[i]);

            var sortSelector = sortSelectors[(int)sortOption];
            orderedQuery = orderedQuery.ThenBy(l => sortSelector(l.data), ascending);

        }

        rootItem.children = orderedQuery.Cast<TreeViewItem>().ToList();
    }

    ///** ********************************************************************************
    //* @summary 初期の並び替え
    //***********************************************************************************/
    IOrderedEnumerable<TextureTreeViewItem> InitialOrder(IEnumerable<TextureTreeViewItem> elements, int[] history)
    {
        EHeaderColumn sortOption = m_SortOptions[history[0]];
        bool ascending = multiColumnHeader.IsSortedAscending(history[0]);
        var sortSelector = sortSelectors[(int)sortOption];
        return elements.Order(l => sortSelector(l.data), ascending);
    }
}

///** ********************************************************************************
//* @summary 拡張メソッド定義
//***********************************************************************************/
static class MyExtensionMethods
{
    public static IOrderedEnumerable<T> Order<T, TKey>(this IEnumerable<T> source, Func<T, TKey> selector, bool ascending)
    {
        if (ascending) // 昇順
        {
            return source.OrderBy(selector);
        }
        else // 降順
        {
            return source.OrderByDescending(selector);
        }
    }

    public static IOrderedEnumerable<T> ThenBy<T, TKey>(this IOrderedEnumerable<T> source, Func<T, TKey> selector, bool ascending)
    {
        if (ascending) // 昇順
        {
            return source.ThenBy(selector);
        }
        else // 降順
        {
            return source.ThenByDescending(selector);
        }
    }
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[SerializeField]属性をプリプロセッサ(#if〜#endif)で括ったらどうなるのか

特に理由のない意地悪がUnity2019.2.2を襲う――!!

using UnityEngine;

public class IfDefIjiwaru : MonoBehaviour
{
#if PLATFORM_STANDALONE
    [SerializeField]
    private string Ijiwaru;
#endif
}

実験

まずは対象のプラットフォームで適当に値を入れてみます。
スクリーンショット 2019-11-01 0.16.21.png
そしておもむろに対象プラットフォーム切り替え。
スクリーンショット 2019-11-01 0.17.11.png
フィールドが消えました。
スクリーンショット 2019-11-01 0.16.48.png
元のプラットフォームに戻すと、入力したはずの値が消えています。
スクリーンショット 2019-11-01 0.21.44.png
ここで諦めてはいけません。
UnityのSceneファイルはYAMLなので、Sublime Textなどのテキストエディタで開けます。
見れば値そのものは残っている様子。
スクリーンショット 2019-11-01 0.23.00.png
ということで、シーンを一度閉じて読み込み直すとこのように値が復帰しました。
スクリーンショット 2019-11-01 0.16.21.png

まとめ

そうなんだ そういうことも あるんだね

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