- 投稿日:2021-03-14T22:38:15+09:00
ROS講座123 UnityでROSと通信する
環境
この記事は以下の環境で動いています。
項目 値 CPU Core i5-8250U Ubuntu 18.04 ROS Melodic Gazebo 9.0.0 python 2.7.17 Unityの動作環境は以下です。
項目 値 ホストマシン Windows10 Unity 2019.4 インストールについてはROS講座02 インストールを参照してください。
またこの記事のプログラムはgithubにアップロードされています。ROS講座11 gitリポジトリを参照してください。概要
これまでQtやブラウザでUIを作ってきましたが、今回はスマホで操作することを目標にしてUnityで作ってみます。
Unityはゲーム作成の統合開発環境で、ゲームで必要な3DCGや物理演算などの処理がを簡単に扱うことが出来ます。UnityはWindows/Mac/CentOSをサポートしていますが、今回はWindows上で開発します。
Unity本体だけでも基本的なゲームを作成することが出来ますが、アセットという追加のソフトウェアパッケージで処理や画像などの素材を追加することが出来ます。アセットはUnityのアセットストアで有償/無償で配布しています。無償の物でもクオリティーが高く便利なものがたくさんあります。今回はROSと通信ができる「ROS#」というアセットを使用します。
またUnityはマルチプラットフォームで実行できることが特徴です。Windowsの統合開発環境上でその場でプレビューすることができて、もちろんそれをWindowsアプリとしてリリースすることもがきます。それ以外にもAndroidやiPoneアプリとしてリリースすることもできます(ただしiPnone用のアプリ作成はMac上でのみ可能)。
Unityでは基本的にC#で開発を行います。UnityではC#プログラムを「スクリプト」と呼びます。
Unity自体に関する説明は長くなってしまうので、入門書にありそうな部分は説明を省きます。ROS#について
UnityとROSの間を通信するためのUnityのアセットとしてROS#があります。これはC#で書かれたRosBridgeのクライアントでUnityでです。このアセットはUnityのアセットストアで配布しているものではなくgithubのページからダウンロードしてUnityでインポートします。
ROS#はrosbridge_serverがjson形式にしたrostopicをwebsocket通信でやり取りをすることでUnityとROSを接続します。インストール(on Windows)
Unity本体のインストール
Unityのダウンロードページからダウンロードします。UnityHubはUnityのバージョンなどの管理ツールで最初は「UnityHub」をダウンロードします。「UnityHub」から「Unity本体」をダウンロードします。最新のROS#の推奨バージョンの2019.4を入れます。
後々必要なのでUnity本体のダウンロードの時に「Android Build Support」にチェックを入れてください。プロジェクトの作成
UnityHubの「新規作成」から新しいプロジェクトを作成します。名前は何でも構いません。テンプレートは「3D」を選びます。
ROS#アセットのプロジェクトへのインストール
- github上のリリースページから「RosSharp.unitypackage」をダウンロードしてローカルに保存します。
メニューバーの「Assets」->「Import Package」->「Custom Package...」を選んで出てくるウィンドウで「RosSharp.unitypackage」を選択します。
Unity上の操作(on Windows)
今回はROSからのimageのsubscribeとjoyのpublishを目標にします。
RosConnectorの設置
「Hierarchy」ウィンドウの「+マーク」->「Create Empty」で空のGameObjectを作成して、RosConnectorと名前を変えます。
「Project」ウィンドウのAssets/RosSharp/Scripts/RosBridgeClient/RosCommuncation/RosConnector.csをRosConnectorにアタッチします。
Imageのsubscribe
まず画像を表示するためのplaneを追加します。「Hierarchy」ウィンドウの「+マーク」->「3D Object」->「Plane」を選択します
「Project」ウィンドウのAssets/RosSharp/Scripts/RosBridgeClient/RosCommuncation/ImageSubscriber.csをRosConnectorにアタッチします。
Joyのpublish
- 「Project」ウィンドウのAssets/RosSharp/Scripts/RosBridgeClient/RosCommuncation/JoyPublisher.csをRosConnectorにアタッチします。
- 「Topic」を/unity/joyとします。
- 「Project」ウィンドウのAssets/RosSharp/Scripts/RosBridgeClient/MessageHandling/JoyAxisReader.csをRosConnectorにアタッチします。
- 「Name」を「Horizontal」とします。
- もう1つ「Project」ウィンドウのAssets/RosSharp/Scripts/RosBridgeClient/MessageHandling/JoyAxisReader.csをRosConnectorにアタッチします。
実行
ネットワーク構成
ROSを実行するUbuntuPCとUnityを実行するWindowsPCが同一セグメント上にいるとします。
以下UbuntuPCのIPアドレスを「192.168.2.105」としますROSの実行(on Ubuntu)
シミュレーションを起動します。
ターミナル1(gazeboの起動)roslaunch sim3_lecture base_world.launchターミナル1(rosbridgeの起動)roslaunch rosbridge_server rosbridge_websocket.launchUnityの実行(on Windows)
UnityEditorのplay(上部中央の三角形のボタン)を押します。
設置したPlaneにカメラの画像が写っています。またROSで
rostopic echo /unity/joy
とすると、Unity側画面でのキーボードの上下左右ボタンの入力が反映されます。コメント
このサンプルではpublish側でで不具合があるために動かない可能性があります。これの場合は以下の修正が必要です。
Assets/RosSharp/Scripts/RosBridgeClient/RosCommuncation/JoyPublisher.csの修正protected override void Start() { System.Threading.Thread.Sleep(3000); // 追加 base.Start(); InitializeGameObject(); InitializeMessage(); }参考
- (書籍)Unity 3D/2Dゲーム開発実践入門 Unity 2019対応版
- C#スクリプトの内容は軽く、Unity特有の内容について詳しく説明しているので1冊でUnityの基本的な開発が大体理解できます。
- ROS#リポジトリ
- ROS#使い方
目次ページへのリンク
- 投稿日:2021-03-14T13:07:31+09:00
VRoid Studioデータのヘアマテリアルを削除する
この記事の趣旨
下記のソフトウェアを作った本人ではあるのですが、この辺りのソフトウェアで既に実装済みであって、これとは別の実装を作りたい人とか、pixivの中の人とか、原理がわからないとツールを使うのが不安の人とか、いろんな立場の人に読んでもらいたいなと思っています。
VRoid運営がなんと言おうと.vroid内の不要マテリアルは削除していい
複製ボタンで一発で増殖したマテリアルは削除ボタン一発で削除できるのがUI/UXとして好ましいし削除機能を実装しないのは(某天才プログラマーさんの言葉を借りるなら)「けしからん」と思うわけです。
間違えてマテリアル増やしちゃうことだってあるわけじゃないですか。そういううっかりって取り消せること国は権利として認められているのですね。民法95条にも書いてある。削除処理の実装の実際
それでは処理を順を追って説明します、
.vroidファイルの構造
VRoidファイルは、ZIPで固めた複数のファイルで構成されますが、説明に必要な部分に絞って表示しております。
Hairishesから参照されている/いないHair-Materialをグループ分けする
髪が参照しているマテリアルというのはhairs_defs.jsonのHairishesの各ノードのParam > _MaterialValueGUIDの値がこれに該当します。
なお、Hairishesは髪グループと髪との2階層構造になっていますが、両方の階層を見る必要があります。ポイント:material_defs.jsonは髪以外のマテリアルも含む
新しめのバージョンで新規作成したモデルは、便宜上、3つの分類が存在することになります。今回は2のみが削除対象となります。
- Hairから参照されているヘアマテリアル
- Hairから参照されていないヘアマテリアル
- 髪以外のマテリアル
髪のマテリアルであることの識別方法は3つ
また髪かそうでないかを見分けるには以下のいずれかのKey-Value値を見れば良いかと思います。
_PrototypeId => "Flora/F00_000_Hair_00_HAIR" _SphereAddTextureId => "/Matcaps/Matcap_RimHair" _Tags => ["HairEditor"]私は_SphereAddTextureIdを使っています。
というのも"Flora/F00_000_Hair_00_HAIR"の部分は条件によって異なる場合があるからです。削除するテクスチャの選定
削除対象のマテリアルノードの _MainTextureId キーに格納されているIDが削除対象のテクスチャとなります。
一応用心のため、削除しないマテリアルから参照されているテクスチャIDが削除対象のテクスチャIDに含まれていないことを確認し、もし重なる部分があれば削除対象から除外しましょう。SQLiteのエントリー削除
テクスチャの管理テーブルはcanvasとlayerの2つのテーブルで十分じゃないかという気もしないでもないのですが、4つのテーブルから構成されています。
正攻法としてはcanvas_idからこれに属するlayer_idを求め、layer_order, layer_info, raster_layer_contentの各テーブルのレコードを削除し、canvas_infoのレコードを削除する流れとなるかと思います。
髪のテクスチャIDはcanvas_infoのIDではなくnameの方に紐づいているのでその点だけ注意。各レイヤーのテクスチャ本体はraster_layer_contentのBLOBに上下反転した状態で入っています。
レンダリング済みテクスチャの削除
rendered_texturesの下にPNGファイルが複数あるかと思いますが、削除対象は先のテクスチャIDに.pngを付けたものが対応しています。
Rubyの実装例
そんなわけで以前実装したものを紹介しておきます。
実はヘアプリセットの出力でもマテリアルは削減している。
実はVRoid Studioのヘアプリセットを出力する処理においても、Hairishesから参照されていないヘアマテリアルのノードとこれに紐づくテクスチャを出力しない処理がなされています。
(出力内容を見る限りでは)等価の処理を再現したのが下記のスクリプトです。こちらも meta.json + hair_defs.json + material_defs.jsonと関連するテクスチャ(PNGファイル)から生成するので、原理としてはかなり近いです。
まとめ
VRoidのサポートページを見る限り、マテリアルを削除できないこと、これを削除できるよう要望があることは十分認識していて、それでもなお実装を後回しにしているのは何かしらの事情がありそうだということ。
困っているユーザーがいるのに「何もしない」という答えは最善ではないと思うので、私はマテリアルを削除する手段を作りました。
- 投稿日:2021-03-14T09:50:24+09:00
[メモ] C#のusingステートメントの使い方
はじめに
C#初心者の私がドメイン駆動設計入門書を読んでいて躓いたので調べた。
リソースの開放
C#では外部リソースとつながったあとは、usingステートメントを用いることでソースが占領しているメモリを開放しメモリリークを防いでいる。
public class UserRepository : IUserRepository { . . . public void Save(User user) { using (var connection = new SqlConnection(connectionString)) using (var command = connection.CreateCommand()) { . . . } } }
- 投稿日:2021-03-14T00:40:47+09:00
WinFormsのTabControlに閉じるボタンを付けるだけ
①とりあえず、TabControl の DrawMode を OwnerDrawFixed にして、文字だけ描画する。
public class MyTabControl : TabControl { public MyTabControl() { DrawMode = TabDrawMode.OwnerDrawFixed; } protected override void OnDrawItem(DrawItemEventArgs e) { var tabRect = this.GetTabRect(e.Index); e.Graphics.DrawString(TabPages[e.Index].Text, Font, new SolidBrush(Color.Blue), new PointF(tabRect.Left + 2, (tabRect.Height - Font.Height))); } }
②閉じるボタンとして使う16x16pxの画像配置用スペースをPaddingで確保する。
public class MyTabControl : TabControl { public MyTabControl() { DrawMode = TabDrawMode.OwnerDrawFixed; Padding = new Point(16, 3); } protected override void OnDrawItem(DrawItemEventArgs e) { var tabRect = this.GetTabRect(e.Index); e.Graphics.DrawString(TabPages[e.Index].Text, Font, new SolidBrush(Color.Blue), new PointF(tabRect.Left + 2, (tabRect.Height - Font.Height))); } }
③DrawImageでCloseボタンを付ける。
2か所に出てくる16はアイコンサイズが16x16pxなので16。public class MyTabControl : TabControl { public MyTabControl() { DrawMode = TabDrawMode.OwnerDrawFixed; Padding = new Point(16, 3); } protected override void OnDrawItem(DrawItemEventArgs e) { var tabRect = this.GetTabRect(e.Index); e.Graphics.DrawString(TabPages[e.Index].Text, Font, new SolidBrush(Color.Blue), new PointF(tabRect.Left + 2, (tabRect.Height - Font.Height))); e.Graphics.DrawImage(Image.FromFile("close.png"), new Point(tabRect.Right - 20, tabRect.Top + (tabRect.Height-16)/2)); } }
④closeボタンがクリックされたらRemoveするようにして完成。
数か所に出てくる16はアイコンサイズが16x16pxなので16。public class MyTabControl : TabControl { public MyTabControl() { DrawMode = TabDrawMode.OwnerDrawFixed; Padding = new Point(16, 3); } protected override void OnDrawItem(DrawItemEventArgs e) { var tabRect = this.GetTabRect(e.Index); e.Graphics.DrawString(TabPages[e.Index].Text, Font, new SolidBrush(Color.Blue), new PointF(tabRect.Left + 2, (tabRect.Height - Font.Height))); e.Graphics.DrawImage(Image.FromFile("close.png"), new Point(tabRect.Right - 20, tabRect.Top + (tabRect.Height-16)/2)); } protected override void OnMouseClick(MouseEventArgs e) { var tabRect = this.GetTabRect(SelectedIndex); var closeButtonRect = new Rectangle(tabRect.Right - 20, tabRect.Top + ((tabRect.Height - 16) / 2), 16, 16); if (closeButtonRect.Contains(e.Location)) { TabPages.RemoveAt(SelectedIndex); } } }
- 投稿日:2021-03-14T00:26:26+09:00
C#でPowerPointを制御することになった不幸なあなたへ
私です
はじめに
とある仕事でパワポ(のスライドショー)を制御する羽目になり、初めはRuby(Win32OLE)で書いていたのですが、その後に別のプログラムをC#で書く必要があって、パワポ制御プログラムだけRubyなのもなと思い始めたのでC#に移植しました。せっかくなのでその知見を書きます。なお、C#はあまり詳しくないです。
基礎知識
どの言語を使うにしても、Windowsで他のアプリを制御すると言ったらOLEです。
OLEってタグで記事書いてる人いませんね。。。すでに滅びた技術なのかなOLE。
OLEとCOMはどちらが古くからある用語かは私もよく知りません。とりあえず以降ではCOMって方を使います。C#でのCOMオブジェクト制御
というわけで「C# OLE」でググりました。引っかかったのがこちらの方の記事。
https://fornext1119.hatenablog.com/entry/20120328/p3一部引用(改行等は修正)
//ワークブックコレクションオブジェクトを生成する。 object excelBooks = excelApp.GetType().InvokeMember( "Workbooks", BindingFlags.GetProperty, null, excelApp, null ); //Excelファイルのオープン object excelBook = excelBooks.GetType().InvokeMember( "Open", BindingFlags.InvokeMethod, null, excelBooks, new object[]{ strMacroPath, System.Type.Missing, System.Type.Missing, System.Type.Missing, System.Type.Missing, System.Type.Missing, System.Type.Missing, System.Type.Missing, System.Type.Missing, System.Type.Missing, System.Type.Missing, System.Type.Missing, System.Type.Missing } );え?まじすか?
というわけで少し前にこちらの記事は見つけていたのですが気乗りしないので移植は放置してました。問題点は2つあります。
- COMオブジェクトのプロパティやメソッドは
InvokeMember
でアクセスする必要があり上記のようにわかりにくい書き方になる。- 「指定しない引数」についても
Type.Missing
を渡す必要がある?1ヘルパークラス作りました
Rubyで書かれたものを移植するのにこんな人間に優しくない表記は嫌なので、せめてもということで以下のヘルパークラスを作りました。
public class OLEHelper { public static object createObject(string progID) { var t = Type.GetTypeFromProgID(progID); return Activator.CreateInstance(t); } public static void freeObject(object o) { Marshal.FinalReleaseComObject(o); } public static object getProperty(object o, string name) { return o.GetType().InvokeMember(name, BindingFlags.GetProperty, null, o, null); } public static void setPropery(object o, string name, object value) { o.GetType().InvokeMember(name, BindingFlags.SetProperty, null, o, new object[]{value}); } // paramsは可変長引数 public static object call(object o, string name, params object[] args) { return o.GetType().InvokeMember(name, BindingFlags.InvokeMethod, null, o, args); } }このヘルパークラスを使うとなんとかSAN値を下げずにパワポ制御プログラムが書けます。
コメントに書いてあるように結局メソッドを呼び出すときも必要な引数だけで大丈夫なようです。class Program { static void Main(string[] args) { var path = Path.GetFullPath(@".\test.pptx"); var powerpoint = OLEHelper.createObject("PowerPoint.Application"); var presentations = OLEHelper.getProperty(powerpoint, "Presentations"); // Openの引数は4つあるが必要な数だけ渡すので大丈夫らしい var presentation = OLEHelper.call(presentations, "Open", path); // presentationsを解放しても開いたpresentationに影響はない OLEHelper.freeObject(presentations); var slideshowsettings = OLEHelper.getProperty(presentation, "SlideShowSettings"); var slideshowwindow = OLEHelper.call(slideshowsettings, "Run"); OLEHelper.freeObject(slideshowsettings); var slideshowview = OLEHelper.getProperty(slideshowwindow, "View"); do { Thread.Sleep(5 * 1000); OLEHelper.call(slideshowview, "Next"); } while ((int)OLEHelper.getProperty(slideshowview, "State") != 5); OLEHelper.call(powerpoint, "Quit"); // オブジェクトを全部解放しないとウインドウが閉じられない OLEHelper.freeObject(slideshowview); OLEHelper.freeObject(slideshowwindow); OLEHelper.freeObject(presentation); OLEHelper.freeObject(powerpoint); } }もう一つ重要な点として、コメントにあるように「全てのCOMオブジェクト」を解放しないとパワポウインドウ、というかプロセスは「パワポ制御プログラム」が終了しても終わりません。
その点で言うと元ネタ記事のプログラムは全オブジェクトを解放してないと思うのでちゃんとExcelが終了するのかちょっと気になります(元ネタのプログラムは動かしてない)素のobjectじゃなくてIDisposable実装したラッパークラス作ったりすると幸せになりそうな気がしますが、実際のプログラムではオブジェクトの生成と破棄が別メソッドに分かれているのでラッパークラスは作りませんでした。
まとめ
- PowerPointなどOfficeを制御したかったらOLE。
- C#(というか.NET)では
InvokeMember
でプロパティやメソッドが呼び出せる。SAN値が下がるのでヘルパークラスかラッパークラスを作ること推奨。