VroidStudioで作った顔と体をBlenderで合成する
BelnderでVroidの顔と体を合わせさせたい!
ということで自分はVroidStudioというツールを使って3Dモデルを作ろうとしました。最初女性アバターで制作して後から男性の体にしたいと思ったのですが、そういう機能は標準になかったのでBlenderで編集したという備忘録です。
BlenderでVRMを扱う場合はこちらの記事を参考にセットアップしてみてください。 【Blender】VRMファイルを扱う 特にBlenderのバージョンに注意してください。
以下はサンプルで
サンプルのVroidの以下の女性アバターと
新規作成で出来る男性アバターです(多少手を加えてますが気にしないで大丈夫です)
VRMプラグインのセットアップが完了したBlenderで操作を行います。
新規でBlenderのプロジェクトを作成します
右にあるCubeってところは消しましょう。
VroidStudioを使って.vrmフォーマットにエクスポートをします。(男女それぞれ)
まずは女性アバターをimportしましょう
色が付いてないのでわかりづらいですね、色をつけましょう! 右上の赤で囲っているところをクリックすれば色がつきます(バージョンによって違うかもしれません)
次に男性アバターもimportします
身長の差が生まれました…が一旦今日は無視します。 女性アバターのデフォルト身長は150cmくらいで男性アバターで ってやると揃いますので参考程度にお願いします。 顔と体を入れ替える際にはVroidStudioの方でそろえておくと簡単です。
今回は適当に辻褄を合わせます
適当な命名をしてるので名前は気にしないでほしいですが男性/女性アバターをimportすると以下のような Scene Collection
となります
ここから女性アバターの Body.baked
を消して男性アバターの Body.baked
を移動させます
これで
首が離れていると思います。
これを少しだけ修正しましょう
顔(Face)と髪(Hair)に当たるところのZ軸を -0.05
しておきます(適宜調整しましょう)
いい感じになりましたね
これをエクスポートします
Unityにimportするとこんな感じですね
UnityChanのアニメーションを適当に再生してみます
動いてますね!!!
ヨシッ!!!!!!!
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のコミット持ってくるとかせずに最新のものを適応させたりしてるのでご了承ください。
以下がリポジトリ
UniVRMをopenupmで導入したらエラーが出る
MToon
というものがないそうです。これでは動かせないので解決していきます。
UnityのProejctで確認しても空のディレクトリとなっていました。
GitHubを確認するとsubmoduleで追加されたディレクトリのようです。
ということはupmはsubmoduleのディレクトリは落とさないようになっているのでしょう。自分で追加する可能性もあるのでそれはそうって感じですね。
Git URL からのインストール という公式に乗っ取りMToonを追加しようとすると
エラーがでました。upmに対応してないようです。なので今回はEditor拡張を作って解決したいと思ったので作りました。
submoduleを持ってくればええやんけ
シンプル単純愚直に考えてsubmoduleをgitで引っ張てくれればええやろと思いました。
Packageのコンテンツは Library/PackageCache/
配下に羅列されます。
当初この各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
がメニューに追加されるので
実行
たぶん以下のようなエラーがでるので Force Quit
します。一度Editorを落としましょう。
そしてEditorを開きなおしてください。
たぶん MToon
が入っていると思います。もし入っていない場合はもう一度 Assets/PackageSubmoduleDownloader
を実行するとエラーなく入ると思います。
まとめ
今回Editorを再起動させない方法を色々考えましたがわかりませんでした。自前で.metaのGUID追加して認識するかな?とか思いましたが「思ったより沼が深かった」案件だったので断念して再起動の手をとっています。
ちなみに ReImport All
などをすると今回のEditor拡張で追加したディレクトリは消えるので再度同じ手順をとる必要がでてきます。
何かと力技ですが自分の一番の目的は MToon
を自分で入れなくてよくなったので満足です。汎用的に書いてるつもりですが何かと穴はありそうです。
こんな力技なアプローチがあるんだな程度にとって頂くといいと思います。
記事を書いて数日後に思ったのですがMtoonをforkしてpackage.json追加したらupmで入れられたのでは…いやはや…いやはや…
勝手ながらforkして package.json
追加いたしました、これPR投げていいのかなぁとOSS慣れしてないのでわかないので自分はこれでMToonを使わせて頂きます🙏
初めてOSSにissueを立てた
タイトル通り
magentaというプロジェクトの中にあるnote-seqというリポジトリにissueを立てました
どういう問題だったのかというと、Pythonの仕様上WindowsではTempディレクトリのファイルを開いた状態でコピー操作などができずエラーが出ます。そのエラーがでる処理が note-seq
にあったということです。
すごく簡単そうな問題ですがWindowsで動かないのは自分が困ったのでissueを立てました。
すると即コメントが返ってきていてOSSのスピード感にビビりました。
そしてissueを立てたのは夜だったので寝ました。起きて対応PRが上がっていることに再びビビる。
対応PRが上がっているのに気づいたのが会社だったので帰って動作確認したところちゃんと動作しました。すごい。
実際に手を動かして問題を解決したわけではないですが自分きっかけで問題が修正されたのは純粋にありがたくうれしかったです。OSSに問題を報告するの大事だなと…いつかPR対応してみたいと思えた。
fastlaneでWindowsのmsbuildを使う
fastlaneでmsbuildを使いたい!
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ライクには書けません。
ここでプラグインの導入を検討しました。
検索をすると上記のプラグインが最初に目につくと思います。 自分もこれでmsbuildが使える!と思い中身を確認したところ、fastalneが元々macOSのみ対応していた名残もあり
この行を見ると引数で実行バイナリのディレクトリを指定するようになっています。引数がなければ msbuild
というPATHの通ったバイナリを実行します。
これはMSBuildの構造の問題もありますが、MSBuidlのexeと同じディレクトリには MSBuild
というディレクトリがあります。そのため、実行バイナリのディレクトリを指定するやり方では MSBuild
ディレクトリを参照してしまい実行できません。そして MSBuild.exe
にパスを通していてもプラグインは msbuild
を呼び出すため実行されません。Windows環境では詰みました…
というわけで改めて探すと
というのを見つけました。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を叩いた方がよさそうでした。ログが出るため。