勉強不足で至らんブログ

勉強不足ですが色々と書いていきます。

VroidStudioで作った顔と体をBlenderで合成する

BelnderでVroidの顔と体を合わせさせたい!

ということで自分はVroidStudioというツールを使って3Dモデルを作ろうとしました。最初女性アバターで制作して後から男性の体にしたいと思ったのですが、そういう機能は標準になかったのでBlenderで編集したという備忘録です。

BlenderVRMを扱う場合はこちらの記事を参考にセットアップしてみてください。 【Blender】VRMファイルを扱う 特にBlenderのバージョンに注意してください。

以下はサンプルで

サンプルのVroidの以下の女性アバターf:id:MakeTake:20210116190352p:plain

新規作成で出来る男性アバターです(多少手を加えてますが気にしないで大丈夫です) f:id:MakeTake:20210116190410p:plain

VRMプラグインのセットアップが完了したBlenderで操作を行います。

新規でBlenderのプロジェクトを作成します f:id:MakeTake:20210116190843p:plain

右にあるCubeってところは消しましょう。

VroidStudioを使って.vrmフォーマットにエクスポートをします。(男女それぞれ)

まずは女性アバターをimportしましょう f:id:MakeTake:20210116191518p:plain

色が付いてないのでわかりづらいですね、色をつけましょう! f:id:MakeTake:20210116191612p:plain 右上の赤で囲っているところをクリックすれば色がつきます(バージョンによって違うかもしれません)

次に男性アバターもimportします f:id:MakeTake:20210116191804p:plain

身長の差が生まれました…が一旦今日は無視します。 女性アバターのデフォルト身長は150cmくらいで男性アバターf:id:MakeTake:20210116191919p:plain ってやると揃いますので参考程度にお願いします。 顔と体を入れ替える際にはVroidStudioの方でそろえておくと簡単です。

今回は適当に辻褄を合わせます

適当な命名をしてるので名前は気にしないでほしいですが男性/女性アバターをimportすると以下のような Scene Collection となります f:id:MakeTake:20210116192042p:plain

ここから女性アバターBody.baked を消して男性アバターBody.baked を移動させます

  1. まずは女性アバターBody を消します f:id:MakeTake:20210116193834g:plain

  2. 男性アバターからBodyを移植してBoneを当てなおします(スカートとかのが残ってますが適宜調整してください) f:id:MakeTake:20210116194116g:plain

これで f:id:MakeTake:20210116194821p:plain

首が離れていると思います。

これを少しだけ修正しましょう f:id:MakeTake:20210116195050p:plain

f:id:MakeTake:20210116195106p:plain

顔(Face)と髪(Hair)に当たるところのZ軸を -0.05 しておきます(適宜調整しましょう)

f:id:MakeTake:20210116195156p:plain

いい感じになりましたね

これをエクスポートします

Unityにimportするとこんな感じですね f:id:MakeTake:20210116195812p:plain

UnityChanのアニメーションを適当に再生してみます f:id:MakeTake:20210116200429g:plain

動いてますね!!!

ヨシッ!!!!!!!

GitHub ActionsについてLTしてました

GitHub Actionsを使ってみた話を社内LTで話してました。

完全版ではないです。完全版には具体的な定義方法などがわかるようにしています。完全版が見たい方は入社すると見れます。

UPMでsubmoduleが追加されないのでsubmodule部分を補填するEditor拡張作ってみた

Unity Advent Calendar 2020の記事です

Unity #3 Advent Calendar 2020 の2日目の記事になります。

Unity#3まであるのすごいですね

Unity Package Managerは便利

Unity Package ManagerでAssetsディレクトリ配下がスッキリするようになってかなり助かっています。そして openupm も使ったりしています。

最近 VRM を使っていて VroidStudio を使ってモデルを作ったのでVRMモデルで遊んでいます。 そこで自分のUnityProjectでVRMモデルを入れて UniVRM を使って制御をしたりしています。

UniVRMは標準のUPMで入れようとしたときにうまくいかなかったのでopenupmで導入しています。

そして、この記事はかなり力技で厳密にsubmoduleのコミット持ってくるとかせずに最新のものを適応させたりしてるのでご了承ください。

以下がリポジトリ

github.com

UniVRMをopenupmで導入したらエラーが出る

f:id:MakeTake:20201114231820p:plain

MToon というものがないそうです。これでは動かせないので解決していきます。 UnityのProejctで確認しても空のディレクトリとなっていました。

f:id:MakeTake:20201114231655p:plain

GitHubを確認するとsubmoduleで追加されたディレクトリのようです。

f:id:MakeTake:20201114232249p:plain

ということはupmはsubmoduleのディレクトリは落とさないようになっているのでしょう。自分で追加する可能性もあるのでそれはそうって感じですね。

Git URL からのインストール という公式に乗っ取りMToonを追加しようとすると

f:id:MakeTake:20201118201220p:plain

エラーがでました。upmに対応してないようです。なので今回はEditor拡張を作って解決したいと思ったので作りました。

submoduleを持ってくればええやんけ

シンプル単純愚直に考えてsubmoduleをgitで引っ張てくれればええやろと思いました。

Packageのコンテンツは Library/PackageCache/ 配下に羅列されます。

f:id:MakeTake:20201114233221p:plain

当初この各Packageの下には .git があってgitで操作できると思ってましたがありません。が、それでもいいので .gitmodule というsubmoduleを管理しているファイルを引っ張れればまだいいと思いましたが消し去られていました。( .gitattribute なども無くなっていたのでgit周り全部取り除かれているみたいです )

そこでファイルを漁っているとupm経由になったことでREADME.mdが追加されており開くと

# VRMShaders

VRM model's supported shaders in Unity.

## Import VRMShaders (Unity 2019.3.4f1~)

`Window` -> `Package Manager` -> `Add package from git URL` and paste `https://github.com/vrm-c/UniVRM.git?path=/Assets/VRMShaders`.

or add the package name and git URL in `Packages/manifest.json`:

{
  "dependencies": {
    "com.vrmc.vrmshaders": "https://github.com/vrm-c/UniVRM.git?path=/Assets/VRMShaders",
  }
}

もろjsonでurlが記載されていたのでREADME.mdから取得するようにしました。

これでGitHubで.gitmoduleを確認してsubmoduleのリポジトリをzipで落として突っ込もうと思います。

実装

実装なんて知らない!ってお方は次の 使い方 まで飛ばしてください。

PackageSubmoduleDownloader.cs(全体)

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine;
using Debug = UnityEngine.Debug;

namespace PackageSubmoduleDownloader
{
    public class PackageSubmoduleDownloader
    {

        private static float progress;
        private static float per = 1;
        private static readonly List<string> submoduleDirectories = new List<string>();

        private static string UnityDirectoryPath = $"{Application.dataPath}/..";
        private static string PackageCachePath => $"{UnityDirectoryPath}/Library/PackageCache";
        private static string TempDirectoryPath = $"{UnityDirectoryPath}/Temp";

        [MenuItem("Assets/Package Submodule Downloader")]
        public static async Task Execute()
        {
            AssetDatabase.DisallowAutoRefresh();
            progress = 0;
            UpdateProgressBar(0.1f, nameof(Execute));
            await DownloadSubmoduleAsync();
            AssetDatabase.Refresh();
            UpdateProgressBar(1 - progress, "Finish");
            AssetDatabase.AllowAutoRefresh();
            EditorUtility.ClearProgressBar();
        }
        
        private static void UpdateProgressBar(float addProgress, string info)
        {
            if (progress + addProgress > 0.9) addProgress = 0;
            progress += addProgress;
            Debug.Log($"[{nameof(PackageSubmoduleDownloader)}] {info} : {progress}");
            EditorUtility.DisplayProgressBar(nameof(PackageSubmoduleDownloader), info, progress / per);
        }
        
        public static async Task DownloadSubmoduleAsync()
        {
            var directories = Directory.GetDirectories(PackageCachePath);
            per = directories.Length;
            foreach (var directory in directories)
            {
                submoduleDirectories.Clear();
                SearchEmptyDirectory(directory);
                var isExitsSubModule = submoduleDirectories.Count != 0;
                if (!isExitsSubModule) continue;
                UpdateProgressBar(0.3f, nameof(GetGitHubUrl));

                var githubUrl = GetGitHubUrl(directory);
                var repositoryName = githubUrl.Split('/').Last();
                var downloadPath = $"{TempDirectoryPath}/{repositoryName}";
                var gitmoduleString = await GitSubmodulesAsync(githubUrl, downloadPath);
                if (gitmoduleString == "") continue;
                UpdateProgressBar(0.3f, nameof(ParseUrls));

                var submoduleUrls = ParseUrls(gitmoduleString);
                var per = submoduleUrls.Count();
                foreach (var url in submoduleUrls)
                {
                    var directoryName = url.Split('/').Last();
                    var tempDirectory = $"{TempDirectoryPath}/{directoryName}";
                    await DownloadRepositoryZipAsync(url, $"{TempDirectoryPath}/{directoryName}");
                    UpdateProgressBar(0.1f / per, nameof(DownloadRepositoryZipAsync));
                    foreach (var subModuleDirectory in from subModuleDirectory in submoduleDirectories let isTargetDirecotry = subModuleDirectory.Contains(directoryName) where isTargetDirecotry select subModuleDirectory)
                    {
                        await UnZipAsync($"{tempDirectory}.zip");
                        UpdateProgressBar(0.1f / per, nameof(UnZipAsync));
                        var source = Directory.GetDirectories(tempDirectory).First();
                        DirectoryMove(source, subModuleDirectory);
                    }
                    UpdateProgressBar(0.1f / per, nameof(DirectoryMove));
                }
            }
        }
        
        private static void SearchEmptyDirectory(string path)
        {
            foreach (var directory in Directory.GetDirectories(path))
            {
                SearchEmptyDirectory(directory);
                var isEmptyFiles = Directory.GetFiles(directory).Length == 0;
                var isEmptyDirectories = Directory.GetDirectories(directory).Length == 0;
                if (isEmptyFiles && isEmptyDirectories)
                {
                    submoduleDirectories.Add(directory);
                }
            }
        }
        
        private static string GetGitHubUrl(string path)
        {
            var readmeFileName = $"{path}/README.md";
            if (!File.Exists(readmeFileName)) return "";
            using (var raedmeFile = new StreamReader(readmeFileName))
            {
                var readme = raedmeFile.ReadToEnd().Split('\n');
                var urlLine = readme.FirstOrDefault(x => x.StartsWith("    \"com."));
                if (urlLine == null) return "";
                // e.g.)    "com.example.package": "https://github.com/example/package",
                return urlLine.Split('"')[3].Split('?').First().Replace(".git", "");
            }
        }

        private static async Task<string> GitSubmodulesAsync(string url, string downloadPath)
        {
            var gitmoduleUrl = $"{url}/master/.gitmodules".Replace("github.com", "raw.githubusercontent.com");
            var isExistsGitModules = await IsExistsGitSubmodulesAsync(gitmoduleUrl);
            if (!isExistsGitModules) return "";
            Directory.CreateDirectory(downloadPath);
            var client = new HttpClient();
            var response = await client.GetAsync(gitmoduleUrl);
            return await response.Content.ReadAsStringAsync();
        }

        private static async Task<bool> IsExistsGitSubmodulesAsync(string url)
        {
            var client = new HttpClient();
            var response = await client.GetAsync(url);
            try
            {
                response.EnsureSuccessStatusCode();
            }
            catch
            {
                // ignored
            }
            return response.IsSuccessStatusCode;
        }

        private static IEnumerable<string> ParseUrls(string gitmoduleString)
        {
            var fileValue = gitmoduleString.Split('\n');
            return fileValue.Where(x => x.StartsWith("  url =")).Select(x => x.Replace("  url = ", "").Replace(".git", ""));
        }

        private static async Task DownloadRepositoryZipAsync(string url, string downloadPath)
        {
            var completionSource = new TaskCompletionSource<TaskStatus>();
            using (var client = new WebClient())
            {
                var lastVersion = await GetLastTagAsync($"{url}/releases/latest/download");
                client.DownloadFileCompleted += (sender, args) =>
                {
                    completionSource.SetResult(TaskStatus.RanToCompletion);
                };
                client.DownloadFileAsync(new Uri($"{url}/archive/{lastVersion}.zip"),  $"{downloadPath}.zip");
            }
            await completionSource.Task;
        }

        private static async Task<string> GetLastTagAsync(string url)
        {
            var client = new HttpClient();
            var response = await client.GetAsync(url);
            try
            {
                response.EnsureSuccessStatusCode();
            }
            catch
            {
                // ignored
            }
            return response.RequestMessage.RequestUri.ToString().Split('/').Last();
        }
        
        private static Task<int> UnZipAsync(string zipPath)
        {
            var completionSource = new TaskCompletionSource<int>();
            var contentName = zipPath.Replace(".zip", "").Split('/').Last();
            var outputPath = $"{TempDirectoryPath}/{contentName}";
#if UNITY_EDITOR_WIN
            var unityEditorDirectory = System.AppDomain.CurrentDomain.BaseDirectory;
            var sevenZipPath = $"{unityEditorDirectory}/Data/Tools/7z.exe";
            var fileName = $"\"{sevenZipPath}\"";
            var arguments = $"x \"{zipPath}\" -o\"{outputPath}\" -r";
#else
            var fileName = "unzip";
            var arguments = $"\"{zipPath}\" -d \"{outputPath}\"";
#endif
            if (Directory.Exists(outputPath)) Directory.Delete(outputPath, true);
            var process = new Process
            {
                StartInfo =
                {
                    FileName = fileName,
                    Arguments = arguments,
                    UseShellExecute = false,
                    CreateNoWindow = true,
                    RedirectStandardOutput = true,
                    RedirectStandardError = true
                },
                EnableRaisingEvents = true
            };
            
            process.Exited += (sender, args) =>
            {
                completionSource.SetResult(process.ExitCode);
                process.Dispose();
            };
            process.OutputDataReceived += (sender, args) => {
                if (!string.IsNullOrEmpty(args.Data)) {
                    Debug.Log(args.Data);
                }
            };
            process.ErrorDataReceived += (sender, args) => {
                if (!string.IsNullOrEmpty(args.Data)) {
                    Debug.LogError(args.Data);
                }
            };
            process.Start();
            return completionSource.Task;
        }
        
        private static void DirectoryMove(string sourceDirectory, string destDirectory)
        {
            var sourceInfo = new DirectoryInfo(sourceDirectory);

            var directories = sourceInfo.GetDirectories();
            Directory.CreateDirectory(destDirectory);
            
            var files = sourceInfo.GetFiles();
            foreach (var file in files)
            {
                var destFileName = Path.Combine(destDirectory, file.Name);

                File.Move(file.FullName, destFileName);
            }

            foreach (var directory in directories)
            {
                var dest = Path.Combine(destDirectory, directory.Name);
                DirectoryMove(directory.FullName, dest);
            }
        }
    }
}

分割してコメントを入れて解説とさせてもらいます。

        // Editorから実行するメソッド
        [MenuItem("Assets/Package Submodule Downloader")]
        public static async Task Execute()
        {
            // 処理が終わるまでEditorの更新がかからないようにする
            AssetDatabase.DisallowAutoRefresh();
            progress = 0;
            UpdateProgressBar(0.1f, nameof(Execute));
            await DownloadSubmoduleAsync();
            AssetDatabase.Refresh();
            UpdateProgressBar(1 - progress, "Finish");
            // 処理が終わるのでEditorの更新をかけていいようにする
            AssetDatabase.AllowAutoRefresh();
            EditorUtility.ClearProgressBar();
        }
        
        // 適当にプログレスバーを出しておく
        private static void UpdateProgressBar(float addProgress, string info)
        {
            if (progress + addProgress > 0.9) addProgress = 0;
            progress += addProgress;
            Debug.Log($"[{nameof(PackageSubmoduleDownloader)}] {info} : {progress}");
            EditorUtility.DisplayProgressBar(nameof(PackageSubmoduleDownloader), info, progress / per);
        }
        
        public static async Task DownloadSubmoduleAsync()
        {
            // UPMの配置場所の取得
            var directories = Directory.GetDirectories(PackageCachePath);
            per = directories.Length;
            foreach (var directory in directories)
            {
                submoduleDirectories.Clear();
                // 空のディレクトリがあればsubmodule候補として列挙する
                SearchEmptyDirectory(directory);
                var isExitsSubModule = submoduleDirectories.Count != 0;
                if (!isExitsSubModule) continue;
                UpdateProgressBar(0.3f, nameof(GetGitHubUrl));

                // README.mdからGitHubのURLを取得する
                var githubUrl = GetGitHubUrl(directory);
                var repositoryName = githubUrl.Split('/').Last();
                var downloadPath = $"{TempDirectoryPath}/{repositoryName}";
                // GitHubのURLから最新の.submoduleを読み取る
                var gitmoduleString = await GitSubmodulesAsync(githubUrl, downloadPath);
                if (gitmoduleString == "") continue;
                UpdateProgressBar(0.3f, nameof(ParseUrls));
                
                // .submoduleに定義されてるurlを列挙する
                var submoduleUrls = ParseUrls(gitmoduleString);
                var per = submoduleUrls.Count();
                foreach (var url in submoduleUrls)
                {
                    var directoryName = url.Split('/').Last();
                    var tempDirectory = $"{TempDirectoryPath}/{directoryName}";
                    // submoduleの最新のzipをUnityのTempにダウンロードする
                    await DownloadRepositoryZipAsync(url, $"{TempDirectoryPath}/{directoryName}");
                    UpdateProgressBar(0.1f / per, nameof(DownloadRepositoryZipAsync));
                    // submoduleの名前と空のディレクトリの名前を突き合わせて合致していたら処理を行う
                    foreach (var subModuleDirectory in from subModuleDirectory in submoduleDirectories let isTargetDirecotry = subModuleDirectory.Contains(directoryName) where isTargetDirecotry select subModuleDirectory)
                    {
                        // ダウンロードしたzipをunzipする
                        await UnZipAsync($"{tempDirectory}.zip");
                        UpdateProgressBar(0.1f / per, nameof(UnZipAsync));
                        var source = Directory.GetDirectories(tempDirectory).First();
                        // tempディレクトリからUPMのディレクトリに移動させる
                        DirectoryMove(source, subModuleDirectory);
                    }
                    UpdateProgressBar(0.1f / per, nameof(DirectoryMove));
                }
            }
        }

以降はメモっておきたいメソッドをピックアップしていきます

        private static string GetGitHubUrl(string path)
        {
            var readmeFileName = $"{path}/README.md";
            if (!File.Exists(readmeFileName)) return "";
            using (var raedmeFile = new StreamReader(readmeFileName))
            {
                var readme = raedmeFile.ReadToEnd().Split('\n');
                // urlのある行はこういう文字列から始まるという力技検索
                var urlLine = readme.FirstOrDefault(x => x.StartsWith("    \"com."));
                if (urlLine == null) return "";
                // こういう文字列が絶対くる前提というの力技処理
                // e.g.)    "com.example.package": "https://github.com/example/package",
                return urlLine.Split('"')[3].Split('?').First().Replace(".git", "");
            }
        }

次はunzip処理

        private static Task<int> UnZipAsync(string zipPath)
        {
            var completionSource = new TaskCompletionSource<int>();
            var contentName = zipPath.Replace(".zip", "").Split('/').Last();
            var outputPath = $"{TempDirectoryPath}/{contentName}";
#if UNITY_EDITOR_WIN
            // Unity.exeの場所を取得してUnity.exe周りにある7-Zipのexeを取得してzip解凍に利用する
            var unityEditorDirectory = System.AppDomain.CurrentDomain.BaseDirectory;
            var sevenZipPath = $"{unityEditorDirectory}/Data/Tools/7z.exe";
            var fileName = $"\"{sevenZipPath}\"";
            var arguments = $"x \"{zipPath}\" -o\"{outputPath}\" -r";
#else
            // unzipはmac/linux系にはあるでしょって前提で書いたもの Unity.app配下にも7z.aとかあったけどlinuxと共通にしたかったのでスルー
            var fileName = "unzip";
            var arguments = $"\"{zipPath}\" -d \"{outputPath}\"";
#endif

Windowsで解凍処理どうしようかなと思ってましたがUnityEngineに付属するならええやろ!って感じで利用しています。 以前にもWebGLのビルドツールにPython環境が丸ごと入っていたりと利用できる環境があったりするので漁ってみるのもおすすめです。 → Unity WebGLのBuild and RunのRunを実行する拡張Pythonについては記載

Unityのバージョンアップで使えなくなる可能性はあるので永続的な保証はありません。

特筆する処理はこれくらいだと思います。

使い方

"com.mizotake.packagesubmoduledownloader": "https://github.com/MizoTake/PackageSubmoduleDownloader.git?path=Assets"

manifest.json に追記すれば追加できます。

Assets/PackageSubmoduleDownloader がメニューに追加されるので

f:id:MakeTake:20201116191340p:plain

実行

たぶん以下のようなエラーがでるので Force Quit します。一度Editorを落としましょう。

f:id:MakeTake:20201118202634p:plain

そしてEditorを開きなおしてください。

f:id:MakeTake:20201116183258p:plain

たぶん MToon が入っていると思います。もし入っていない場合はもう一度 Assets/PackageSubmoduleDownloader を実行するとエラーなく入ると思います。

まとめ

今回Editorを再起動させない方法を色々考えましたがわかりませんでした。自前で.metaのGUID追加して認識するかな?とか思いましたが「思ったより沼が深かった」案件だったので断念して再起動の手をとっています。 ちなみに ReImport All などをすると今回のEditor拡張で追加したディレクトリは消えるので再度同じ手順をとる必要がでてきます。

何かと力技ですが自分の一番の目的は MToon を自分で入れなくてよくなったので満足です。汎用的に書いてるつもりですが何かと穴はありそうです。

こんな力技なアプローチがあるんだな程度にとって頂くといいと思います。

記事を書いて数日後に思ったのですがMtoonをforkしてpackage.json追加したらupmで入れられたのでは…いやはや…いやはや…

github.com

勝手ながらforkして package.json 追加いたしました、これPR投げていいのかなぁとOSS慣れしてないのでわかないので自分はこれでMToonを使わせて頂きます🙏

初めてOSSにissueを立てた

タイトル通り

magentaというプロジェクトの中にあるnote-seqというリポジトリにissueを立てました

どういう問題だったのかというと、Pythonの仕様上WindowsではTempディレクトリのファイルを開いた状態でコピー操作などができずエラーが出ます。そのエラーがでる処理が note-seq にあったということです。

すごく簡単そうな問題ですがWindowsで動かないのは自分が困ったのでissueを立てました。

github.com

すると即コメントが返ってきていてOSSのスピード感にビビりました。

そしてissueを立てたのは夜だったので寝ました。起きて対応PRが上がっていることに再びビビる。

対応PRが上がっているのに気づいたのが会社だったので帰って動作確認したところちゃんと動作しました。すごい。

実際に手を動かして問題を解決したわけではないですが自分きっかけで問題が修正されたのは純粋にありがたくうれしかったです。OSSに問題を報告するの大事だなと…いつかPR対応してみたいと思えた。

fastlaneでWindowsのmsbuildを使う

fastlaneでmsbuildを使いたい!

docs.fastlane.tools

fastlaneというモバイル向けのビルドステップを簡略化してくれるruby製のツールがあります。プラグインなどが簡単に導入でき柔軟でとてもよいツールです(SlackやCDツールとの連携が楽)

fastlaneは数年前までmacOS専用のiOSのみのツールでしたがWindows/Linuxにも対応し、Androidのサポートも加わっています。使い方によってはモバイルに限りません。

今回はモバイルであってもなくてもVisualStudioで開発する際にMSBuildコマンドが使えればfastlaneで多少楽にビルドステップが書けると思います。

そんなツールでmsbuildを使いWindowsプラットフォームでビルドする想定のメモ書きです。

msbuildを使うには

fastlaneはmsbuildの実行をデフォルトではサポートしていません。 fastlaneはshの実行も簡単にできるので何でもshで書けます。msbuildも全てshにまとめていると実行できます(gitbashやcygwin導入時)

ただ、shで実行するだけではfastlaneライクには書けません。

ここでプラグインの導入を検討しました。

github.com

検索をすると上記のプラグインが最初に目につくと思います。 自分もこれでmsbuildが使える!と思い中身を確認したところ、fastalneが元々macOSのみ対応していた名残もあり

https://github.com/willowtreeapps/fastlane-plugin-msbuild/blob/master/lib/fastlane/plugin/msbuild/actions/msbuild_action.rb#L9

この行を見ると引数で実行バイナリのディレクトリを指定するようになっています。引数がなければ msbuild というPATHの通ったバイナリを実行します。 これはMSBuildの構造の問題もありますが、MSBuidlのexeと同じディレクトリには MSBuild というディレクトリがあります。そのため、実行バイナリのディレクトリを指定するやり方では MSBuild ディレクトリを参照してしまい実行できません。そして MSBuild.exe にパスを通していてもプラグインmsbuild を呼び出すため実行されません。Windows環境では詰みました…

というわけで改めて探すと

github.com

というのを見つけました。Xamarinは内部的にmsbuildを使うところがあるので中身を確認したところ。引数で msbuild の実行バイナリを指定する形式でしたのでexeも指定できました!

これで一旦めでたし!なのです!が!

msbuildのコマンドオプションの引数が…ない…だと…

という事態に陥ったのですが、fastlaneの作りとして裏でターミナルなどにコマンドを渡しているだけではあるので

lane :release do
  option = " /m"
  msbuild(
    msbuild: "exeまでのパス",
    project: "slnまでのパス",
    target: "ビルドするターゲット" + option 
  )
end

のようにオプションのパラメータではない引数の後ろに空白を入力してオプションを指定すれば動きます!解決!(本当はissue作ってPRを作ったりした方がいいと思いますが)

おわり

fastlaneのmsbuildメモでした。Windows対応などがまだ浸透していないことを考えるとmacOSのみでビルド環境を構築した方がいいのかもしれません。ただ自分のようにWindowsでやりたい!という人は工夫をすれば乗り越えれると思います! rubyは割と簡単に扱えるので自分でプラグインを書くのもいいかもしれませんね!

追記(2020/12/01)

上記で紹介しているlaneを使うとログが正常に出ない場合がありshのlaneでMSBuild.exeを叩いた方がよさそうでした。ログが出るため。

UnityのStreamingAssets内でPython環境構築してみた

Pythonで作られたライブラリなどをUnityでも流用したい!

ということで思いついたのがPythonに必要なファイルを全て StreamingAssets に突っ込む方式でした。

その前にクライアント/サーバーでやり取りしろよ!って話ではあります。はい。その通りです!ただスタンドアローンで動かしたかったんです!!

PythonC#のやり取りをする

github.com

C#との連携で他にもツールは出てきたのですが最終的にPythonのライブラリも使いたいと思っており、探すとPythonnetならできるという情報があったので選出しました。

GithubWikiに導入方法などは書いておりスムーズに導入できました。dllをダウンロードしてPluginsディレクトリに入れるだけですね。

中身を弄る必要があればGitHubから簡単にcloneしてdefineを弄ればUnityで動かすカスタムができる印象でした。

突っ込むPythonの環境を取得する

Pythonの環境はAnacondaで取得した環境をStreamingAssetsに入れることにしました。 Pythonのインストールパスを確認してから

Python
|---------DLLs
|---------Lib
|---------Scripts
|-たくさんのdllとpyファイル

をUnityのStreamingsAssetsに入れます。基本的に必要なものは上記でした。他のディレクトリは無くても大丈夫です。

Editorでは動きます。

Editor/Buildの違い

Unity Editorでは Plugins ディレクトリ以外にdllがあっても読み込んでくれます。そのため StreamingAssetsPythonのすべてを入れておくだけでPythonが動きます。しかし、BuildをしてStreamingAssetsにPythonを入れてもdllを検知できません。そのため、Pythonを動かすのに必要なdllをPluginsにいれて他の.pyなどをStreamingAssetsに入れることで解決しました。

そのためdllを全てPluginsに入れて他をStreamingAssetsに入れるようにします。細かいことを気にしなければ全部 PluginsStreamingAssets に入れてしまいます。

ちゃんと分けたい場合は

StreamingAssets

find . -name "*.dll" -type f | xargs rm

などをして必要な拡張子だけを残すようにします。 Plugins だと exepy など dll 以外を消す必要があります。(Windowsだと)

動かした結果

using周りは端折りますが以下のコードを動かしました。(.Net4.0環境)

public class PythonLifeCycle : IDisposable
    {

        public void Initialize()
        {
            
            var pythonHome = $"{Application.streamingAssetsPath}/PythonEnv";


            var scripts = $"{pythonHome}/Scripts";
            
            Environment.SetEnvironmentVariable("PATH", $"{pythonHome};{scripts}", EnvironmentVariableTarget.Process);
            Environment.SetEnvironmentVariable("DYLD_LIBRARY_PATH", $"{pythonHome}/Lib", EnvironmentVariableTarget.Process);

            PythonEngine.PythonHome = $"{pythonHome}";
            PythonEngine.PythonPath = $"{pythonHome}/Dlls;{pythonHome}/Lib;{pythonHome}/Lib/site-packages";
            
            PythonEngine.Initialize();
        }

        public void Dispose()
        {
            PythonEngine.Shutdown();
        }
private readonly PythonLifeCycle lifeCycle = new PythonLifeCycle();
        [SerializeField] private TextMeshProUGUI textUI;

        void Start()
        {
            lifeCycle.Initialize();

            RunPython();
        }

        private void RunPython()
        {
            using (Py.GIL())
            {               
                dynamic sysModule = Py.Import("sys");
                
                textUI.text = sysModule.version;
            }
        }

        private void OnDestroy()
        {
            lifeCycle.Dispose();
        }

シンプルにPythonの環境を構築してTextを出力するようにしています。

Editor Windows
f:id:MakeTake:20200906101448p:plain f:id:MakeTake:20200905224738p:plain

最低限Editor/Buildでの差は埋めれました。 他(WebGL/Android)はそのまま動かなかったので放置しています。一旦はWinのみで開発するつもりだったので。 触った感じだとデスクトッププラットフォームだと問題ないと思います。

WebGLで実行とエラーがでるのですがパッとうまい解決策が見つけれず放置 Androidでビルドしてgradleの処理の際にStreamingAssetsのファイルが多すぎてファイルをすべてgroovyのファイルに記述されていて、その記述されている数が多いことでエラーとなり失敗します。これはzipに纏めるなりの回避をすればなんとかなるとは思います。(メモ)

まとめ

今後はpythonのライブラリを導入してC#から使えるようにしていこうと思います。1プラットフォームでもPythonを内蔵して動いて良かったです。

magentaセットアップをwslでしたら躓いてDockerの良さを再認識したメモ

音の自動生成を試したい!

ということでgoogleさんが作っているmagentaというツールを使おうと思いセットアップをしてみました。結構躓いたのでメモ

github.com

↓の手順で触っていきました。

qiita.com

1 Windowsで行うためwslの設定から

www.kkaneko.jp

pipでインストールすると新しいtensorflowのサポートがされておらず以下の公式サイトをみつつ情報をあつめ進めていきました。

www.tensorflow.org

2 ハンズオンを触っていく

ここから出会ったエラーと解決を書いていますが 3 本番はここからだった へ飛べばスキップできるものです。さくさくセットアップしたい場合は飛ばしてください。

省略可能領域

python scripts/data/create_note_sequences.py を行おうとするとそもそもtensorflowのバージョンに合っていないので

    def log_statistics_list(stats_list, logger_fn=tf.logging.info):
AttributeError: module 'tensorflow' has no attribute 'logging'

みたいなエラーとなる

pipには既にtensorflow1系は提供されていないのでpipでインストールした場所 /home/{ユーザー名}/.local/lib/python3.8/site-packages/magenta/ に行って中身の必要箇所を置き換える手段をとりました

すると

    class NoteSequenceRecordWriter(tf.python_io.TFRecordWriter):
AttributeError: module 'tensorflow' has no attribute 'python_io'

という別のエラーが出てきたので、さっきの箇所は通ったようです、同じようにエラーに対応していきます。

基本的には tf.hogetf.compat.v1.hoge にすれば通ります。

この調子で進めると

‘‘‘ AttributeError: module 'tensorflow' has no attribute 'contrib' ‘‘‘

というエラーで行き詰ってしまいました。調べると Migrate your TensorFlow 1 code to TensorFlow 2  |  TensorFlow Core

一切のサポートが切られていて、tensorflow-addonというのも試しましたがうまくいきませんでした。

3 本番はここからだった

そこで、ふとmagentaのReleaseを確認したらtensorflow2のサポートのsourcecodeが貼ってあり

Releases · magenta/magenta · GitHub

ひとまず Magenta v2.0.1 をダウンロードして、pipでインストールしていた場所 /home/{ユーザー名}/.local/lib/python3.8/site-packages/magenta/ で中身を全部置き換えると違うエラーとなり進んだようです。

pipでインストールしたmagenta自体を置き換えたので 2 ハンズオンを触っていく の労力は意味なかったようです。pipのパッケージ更新してくれんかな…

ModuleNotFoundError: No module named 'tensorflow_probability'

pipで tensorflow_probability をインストールすれば解決

ModuleNotFoundError: No module named 'tensor2tensor'

pipで tensor2tensor をinstallしようとすると

Collecting pygame
  Using cached pygame-1.9.6.tar.gz (3.2 MB)
    ERROR: Command errored out with exit status 1:

となりpygameというものでコケてしまう 調べると Python - pythonのライブラリをインストールしたいのですが、エラーが出てしまい進みません|teratail

を発見、Python3.8を使っているから良くないみたいなのでpyenvを使ってPython3.7に切り替えると無事成功

そして最初のコマンドは通ったので python scripts/data/convert_to_melody_dataset.py --config attention_rnn を実行していく

AttributeError: module 'tensorflow' has no attribute 'app'

が再び浮上、今回はmagenta_sessionの方で出ているようだったのでmagenta_sessionのディレクトリを全検索して tf.app から tf.compat.v1.app で対処

magenta_session/convert_to_melody_dataset.py at master · icoxfog417/magenta_session · GitHub

    pipeline_instance = md.get_pipeline(config, md.FLAGS.eval_ratio)

get_pipeline が無いと言われるので調べると

    pipeline_instance = md.melody_rnn_pipeline.get_pipeline(config, md.FLAGS.eval_ratio)

にすることで解決

TypeError: 'float' object is not iterable

magenta_session/convert_to_melody_dataset.py at master · icoxfog417/magenta_session · GitHub

        md.pipeline.tf_record_iterator(tgt.SEQUENCE_FILE, pipeline_instance.input_type),

tf_record_iteratorは使われなくなるそうで今後は

tf.data.TFRecordDataset  |  TensorFlow Core v2.3.0

を使うようにするのが良いそうでした

        tf.data.TFRecordDataset(tgt.SEQUENCE_FILE, pipeline_instance.input_type),

に置き換えて通った

2020-08-10 19:54:24.507256: E tensorflow/stream_executor/cuda/cuda_driver.cc:314] failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected

が出てきて初CUDA実行の気配

ここで色々躓いて、そもそも環境のセットアップがきちんと行えておらずwsl側でgpuを認識していないようでした。driverのインストール/アンインストールなどもしたのですが改善せず。

待ってました CUDA on WSL2 - Qiita

を頼りに色々やってみたのですがWindowsがプレビューじゃないからぽいのでWindowsのダウンロードを待ちながらmagentaにはdockerもあるので、そちらを試したところ秒で処理が動いたのっでもうDockerでいいんじゃないかと思いました。wslでgpuを使いたい気持ちがあったものの結構格闘してすり減っていたのでDocker万歳ということで動かしていきます。(Windowsプレビューでも動かなかったのでもうわからないからやっぱDocker最高)

追記 (2020/08/12) wslのバージョンが足りてなかった、MicrosoftStoreからインストールが何故かできなかったのでマニュアルインストールの手段をとる docs.microsoft.com

dockerのセットアップがうまくいかず

Windows Subsystem for Linux(WSL)で Docker を利用する - simplestarの技術ブログ

Repository configuration | nvidia-docker

この2つの記事を参考にセットアップして動くことを確認

docker runするとエラーが出ていて

cgroups: cannot find cgroup mount destination

Docker command でドッカー練習する時のメモ | Hapicode

を参考に解決

TensorFlowからGPUが認識できているかを2行コードで確認する - 動かざることバグの如し

などをみてgpuの認識を確認

sudo mkdir /sys/fs/cgroup/systemd
sudo mount -t cgroup -o none,name=systemd cgroup /sys/fs/cgroup/systemd

すると動いた

以下のサイトでDockerfileのベースを探すとcuda周りのセットアップをしなくていいので最高

docs.nvidia.com

おわり

セットアップまでにかなり引っかかるポイントがあり折れました。自分の環境、セットアップの順番で起こったことなので他の人の環境だとまた違うことが起こる可能性は高いですが、何か参考になればと思います。(最初からDocker使う方が時短にはなる)

また個人的にjetbrains系のエディターをかなりつかうのでwsl環境と同期する方法があるのか調べると Configure an interpreter using WSL - Help | PyCharm あったのでjetbrainsを使ってwslで環境構築が終わった人