- 投稿日:2019-10-21T22:37:33+09:00
【Nuget】C#の自作ライブラリをNugetパッケージ化する方法
社内業務改善において、WindowsデスクトップアプリケーションをC#で開発することは結構有用だと思っています。
その中で、色々な社内アプリケーションで共通部分の設計っていっぱいあるよなあってよく感じています。
例えば
- 社員番号から個人情報(氏名・IDなど)を入手
- 社内メールを配信
- 部品リストの操作
等。
こういう同じような機能を社内の色々な人が開発するのは無駄なので、C#で自作したライブラリをNugetで配布する方法をメモしておきます。
Nuget
Visual Studioにおけるパッケージ管理ツール。
インストールしたライブラリが依存しているライブラリも自動でインストールしてくれるので便利。用意するもの
ビルドしたライブラリをNugetパッケージ化するソフト。コマンドラインから使用します。
- ビルドした.NETFrameworkライブラリ
当たり前ですが。
準備
- nuspecファイルを生成
Nugetパッケージ化はプロジェクトと同名の.nuspecファイルに基づいて行われますので、nuspecファイルを用意します。その正体はxmlファイル。
下記のコマンドで生成します。nuget specすると、Package.nuspecというファイルが生成されます。
中身はこんなの<?xml version="1.0"?> <package > <metadata> <id>Package</id> <version>1.0.0</version> <authors></authors> <owners></owners> <licenseUrl>http://LICENSE_URL_HERE_OR_DELETE_THIS_LINE</licenseUrl> <projectUrl>http://PROJECT_URL_HERE_OR_DELETE_THIS_LINE</projectUrl> <iconUrl>http://ICON_URL_HERE_OR_DELETE_THIS_LINE</iconUrl> <requireLicenseAcceptance>false</requireLicenseAcceptance> <description>Package description</description> <releaseNotes>Summary of changes made in this release of the package.</releaseNotes> <copyright>Copyright 2019</copyright> <tags>Tag1 Tag2</tags> <dependencies> <dependency id="SampleDependency" version="1.0" /> </dependencies> </metadata> </package>内容のリファレンスはこちら
このファイル名を「プロジェクト名.nuspec」にして、プロジェクトのルートフォルダに入れておきます。(ソリューションに登録しておくと楽かも?)
発行する
発行するのは下記コマンド
nuget pack [パッケージ化するディレクトリ]Nugetパッケージが生成されるので、無事発行完了。
おまけ
社内でNuget配布するには?
ファイル共有サーバーのどのフォルダをNuget用のフォルダにするかを社内で決めておけば十分です。
Visual Studioのパッケージソースでそのフォルダを登録しておけば、Nugetがいい感じに一覧・バージョン表示してくれるので、発行したパッケージをホイホイそのフォルダにぶち込んでいけばOK。
- 投稿日:2019-10-21T19:02:37+09:00
【Unity】Noitom Hi5とPerception Neuron PROを使った全身トラッキング環境の構築(+モーション記録)
最初に
Hi5とPerception Neuron PROはVTuberハッカソン 全国ツアー2019に参加した際に借りた機材になります。
個人で所有していないため、本機材に関する質問には、返答できない場合があります。
ご了承ください。本記事の目標
Unity上でVRM形式のモデルをNeuron、Hi5で動かし、そのモーションを記録する
環境
【PC】Windows10 1903
【Unity】2019.2f1トラッカー
SDK
- Noitom Perception Neuron PRO
ダウンロードページお借りしたモデル
- 折岸みつ
モデルページお借りしたスクリプト
モデルインポート
- UniVRM
ソースコードモーショントラッキング関係
モーション記録関係
- EasyMotionRecorder
ソースコード設定
Noitom Perception Neuron PRO SDK
ダウンロードページからAXIS NEURON PROをダウンロード、インストールし起動する
[File]->[Settings]->[Output Format]のBVH Dataの項目のDisplacementの項目のチェックを外す
OKで設定を反映
Unity
モデルインポート
UniVRMリリースページから最新版の.unitypackageをダウンロード
UnityのプロジェクトにUnity Packageをインポート
VRM形式のモデルデータをAssetフォルダ直下にコピペする
UniVRMがUnity用にプレハブを作成する
Noitom Perception Neuron PRO
Perception Neuron PRO、Hi5設定ツールをダウンロードし、解凍する
解凍後のフォルダのAssetフォルダから、
・Neuronフォルダ
・NotionHi5フォルダ
・calibration.unity
をUnityプロジェクトのAsset直下にコピペする
動かしたいモデルにNeuronAnimator.csをアタッチする
設定完了
気になる人はUnityを再生して、AXIS NEURON PRO上の動きがモデルに反映されているか確認するNoitom Hi5
上記Noitom Perception Neuron PROの項目1.、2.を行っていない人は行う
モデルの右手首、左手首にHi5_InertiaInstance.csをアタッチ
それぞれを以下の画像のように設定する
設定が面倒な方はHi5_InertiaInstance.csの設定が面倒な人向けの項目を確認してください
左手の設定サンプル(右手はそれぞれを右手のモデルに読み替えてください)
Hi5_InertiaInstance.csの設定が面倒な人向け
自動設定機能を追加します。
- Hi5_InertiaInstance.csにhttps://gist.github.com/neon-izm/395709df5af70021490625e4c03e59bdより引用した以下のコードを加える
/// <summary> /// モデルを変えたときに HandBone をセットし直すのが面倒だったため、自動的にアタッチしてくれる関数 /// HandBones[0] に対象モデルのRoot(Animatorがアタッチされてるオブジェクト)を入れてから実行する。 /// </summary> [ContextMenu("Automatic Set HandBone")] void AutomaticSetHandBone() { if (HandBones[0] == null) { Debug.LogError("HandBones[0] にモデルのRoot(Animatorとかあるオブジェクト)を入れてください。"); return; } //Animator からボーン情報を持ってきたいので、Animator を取得 var animator = HandBones[0].GetComponent<Animator>(); if (animator == null) { Debug.LogError("Animator が見つかりません。"); return; } //念の為初期化 HandBones = new Transform[(int) Bones.NumOfHI5Bones]; //左手と右手で取得すべきボーンが違うから判定 switch (HandType) { case Hand.LEFT: HandBones[1] = animator.GetBoneTransform(HumanBodyBones.LeftHand); HandBones[2] = animator.GetBoneTransform(HumanBodyBones.LeftThumbProximal); HandBones[3] = animator.GetBoneTransform(HumanBodyBones.LeftThumbIntermediate); HandBones[4] = animator.GetBoneTransform(HumanBodyBones.LeftThumbDistal); HandBones[6] = animator.GetBoneTransform(HumanBodyBones.LeftIndexProximal); HandBones[7] = animator.GetBoneTransform(HumanBodyBones.LeftIndexIntermediate); HandBones[8] = animator.GetBoneTransform(HumanBodyBones.LeftIndexDistal); HandBones[10] = animator.GetBoneTransform(HumanBodyBones.LeftMiddleProximal); HandBones[11] = animator.GetBoneTransform(HumanBodyBones.LeftMiddleIntermediate); HandBones[12] = animator.GetBoneTransform(HumanBodyBones.LeftMiddleDistal); HandBones[14] = animator.GetBoneTransform(HumanBodyBones.LeftRingProximal); HandBones[15] = animator.GetBoneTransform(HumanBodyBones.LeftRingIntermediate); HandBones[16] = animator.GetBoneTransform(HumanBodyBones.LeftRingDistal); HandBones[18] = animator.GetBoneTransform(HumanBodyBones.LeftLittleProximal); HandBones[19] = animator.GetBoneTransform(HumanBodyBones.LeftLittleIntermediate); HandBones[20] = animator.GetBoneTransform(HumanBodyBones.LeftLittleDistal); break; case Hand.RIGHT: HandBones[1] = animator.GetBoneTransform(HumanBodyBones.RightHand); HandBones[2] = animator.GetBoneTransform(HumanBodyBones.RightThumbProximal); HandBones[3] = animator.GetBoneTransform(HumanBodyBones.RightThumbIntermediate); HandBones[4] = animator.GetBoneTransform(HumanBodyBones.RightThumbDistal); HandBones[6] = animator.GetBoneTransform(HumanBodyBones.RightIndexProximal); HandBones[7] = animator.GetBoneTransform(HumanBodyBones.RightIndexIntermediate); HandBones[8] = animator.GetBoneTransform(HumanBodyBones.RightIndexDistal); HandBones[10] = animator.GetBoneTransform(HumanBodyBones.RightMiddleProximal); HandBones[11] = animator.GetBoneTransform(HumanBodyBones.RightMiddleIntermediate); HandBones[12] = animator.GetBoneTransform(HumanBodyBones.RightMiddleDistal); HandBones[14] = animator.GetBoneTransform(HumanBodyBones.RightRingProximal); HandBones[15] = animator.GetBoneTransform(HumanBodyBones.RightRingIntermediate); HandBones[16] = animator.GetBoneTransform(HumanBodyBones.RightRingDistal); HandBones[18] = animator.GetBoneTransform(HumanBodyBones.RightLittleProximal); HandBones[19] = animator.GetBoneTransform(HumanBodyBones.RightLittleIntermediate); HandBones[20] = animator.GetBoneTransform(HumanBodyBones.RightLittleDistal); break; default: Debug.LogError("HandType が不正なものです。"); break; } }
下図の通り、
・Hand Baseの要素0にVRMのアニメーターがついているオブジェクトを設定
・自動設定を実行
・追加の設定
を行う
Asset直下のcalibration.unityを再生し、Hi5のキャリブレーションを行う(スペースキーで画面遷移します)
詳細な手順は以下の動画をご確認ください
https://youtu.be/FN1wvcpdOjk?t=157レコーディング環境
1.EasyMotionRecorderリリースページから最新版のunitypackageをダウンロード、プロジェクトにインポート
2.Asset -> EasyMotionRecorder -> Prefubs -> EasyMotionRecorderをシーンにドラッグアンドドロップ
3.オブジェクトEasyMotionRecorderのMotionDataRecoder.csのアニメーターにモデルをドラッグアンドドロップ
動作
上記設定の項目を行った状態で再生すると、Hi5とPerception Neuron PROを着用した人のモーションがモデルに反映されます。
デモ用に取ったモーションを再生したもの
レコーディング
記録
Unityを再生中にEasyMotionRecorderのMotionDataRecorder.csに設定したキーで録画開始、終了ができます。
記録されたモーションはAsset -> Resourcesに記録されます。アニメーションに変換
そのままでは、アニメーターで再生できないので、アニメーションに変換します。
Asset -> Resourcesに記録された.assetファイルを選択し、下図の通りHumanoid Animation Clipとしてエクスポートします。
あとは、アニメーションコントローラーに張り付けて使用してください。参考文献
Noitom Perception Neuron PROホームページ
https://www.aiuto-jp.co.jp/products/product_2459.php
Neuron PROの着方やSDKのダウンロードでお世話になりました유니티 라이브 모션 캡쳐 with 하이파이브 글러브 & 뉴런 프로
https://www.youtube.com/watch?v=FN1wvcpdOjk
Unity側のNeuron PRO、Hi5の設定、ツールのダウンロードでお世話になりましたizm_11's blog
Noitom Hi5をVTuber案件で投入したのでレビューしつつ便利スクリプトを書いた
http://izm-11.hatenablog.com/entry/2018/10/10/184502
Hi5の自動設定のコードをお借りしました。EasyMotionRecorder
http://github.com/duo-inc/EasyMotionRecorder
モーションの記録でお世話になりました。UniVRM
https://github.com/vrm-c/UniVRM
VRMモデルのインポートでお世話になりました。ニコニ立体 【モデル配布】折岸みつ【オリキャラ】
https://3d.nicovideo.jp/works/td35076
モデルをお借りしました
- 投稿日:2019-10-21T18:18:06+09:00
【Unity(C#)】Emptyなオブジェクトを可視化してEdit簡略化
Transformだけのオブジェクト
Create EmptyしたObjcetって文字通りメッシュも何もないので、
位置調整が必要なオブジェクトとして設定してしまった場合、微調整がけっこう大変ですよね。AudioSourceだけのオブジェクトで音源の位置を変化させた場合なんかも、
視覚情報だけでデバッグしたいところです。そこで、ギズモを描画する機能を使って編集、デバッグをしやすくします。
もうすでにきれいにまとめてくださっている方がいらっしゃったので、らくちんでした。
UnityのGizmosを使ってシーンビューで視覚的なデバッグの補助をするコード
#if UNITY_EDITOR using UnityEditor; using UnityEngine; /// <summary> /// ギズモちゃん /// </summary> public class DrowGizmo : MonoBehaviour { [SerializeField] enum MODE { CUBE, WIRECUBE, SPHERE, WIRESPHERE } [SerializeField,Header("描画モード選択")] MODE m_mode; [SerializeField, Range(0,5),Header("ギズモの大きさ")] float m_gizmoSize = 0.3f; [SerializeField, Header("ギズモの色")] Color m_gizmoColor = new Color(1f, 0, 0, 0.3f); void OnDrawGizmos() { Gizmos.color = m_gizmoColor; Vector3 thisObjPos = this.gameObject.transform.position; switch (m_mode) { case MODE.CUBE: Gizmos.DrawCube(thisObjPos,Vector3.one*m_gizmoSize); break; case MODE.SPHERE: Gizmos.DrawSphere(thisObjPos, m_gizmoSize); break; case MODE.WIRECUBE: Gizmos.DrawWireCube(thisObjPos, Vector3.one * m_gizmoSize); break; case MODE.WIRESPHERE: Gizmos.DrawWireSphere(thisObjPos, m_gizmoSize); break; } } [CustomEditor(typeof(DrowGizmo))] public class CustomWindow : Editor { public override void OnInspectorGUI() { EditorGUILayout.BeginVertical(GUI.skin.box); { EditorGUILayout.HelpBox("ギズモでEditサポート", MessageType.Info); } EditorGUILayout.EndVertical(); base.OnInspectorGUI(); } } } #endifOnDrawGizmos
OnDrawGizmos
内に書いた処理はオブジェクトを選択していなくても表示されます。
基本こちらしか使わないかな~?と思います。
- 投稿日:2019-10-21T15:29:32+09:00
C# LINQメソッド一覧
要素の取得(単一)
該当の要素が無い場合
OrDefaultを付けていないメソッドは例外をスローする
OrDefaultを付けたメソッドは型の既定値を返すvar source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.ElementAtOrDefault(8); // -> 0ElementAt
指定した位置(インデックス)にある要素を返す
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.ElementAt(2); // -> 52First
条件に合う最初の要素を返す
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.First(e => e > 15); // -> 16Last
条件に合う最後の要素を返す
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.Last(e => e > 15); // -> 21Single
唯一の要素を返す
該当する要素が複数ある場合例外をスローするvar source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.Single(e => e > 50); // -> 52
要素の取得(複数)
Where
条件を満たす要素を全て返す
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.Where(e => e > 20); // -> {52, 21}Distinct
重複を除いたシーケンスを返す
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.Distinct(); // -> {11, 16, 52, 21, 8}Skip
先頭から指定された数の要素をスキップし、残りのシーケンスを返す
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.Skip(3); // -> {21, 8, 8}SkipWhile
先頭から指定された条件を満たさなくなるまで要素をスキップし、残りのシーケンスを返す
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.Skip(e => e < 20); // -> {52, 21, 8, 8}Take
先頭から指定された数の要素を返す
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.Take(3); // -> {11, 16, 52}TakeWhile
先頭から指定された条件を満たす要素を返す
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.TakeWhile(e => e < 20); // -> {11, 16}
集計
Max
最大値を返す
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.Max(): // -> 52Min
最小値を返す
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.Min(): // -> 8Average
平均値を返す
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.Average(); // -> 19.3333333333333Sum
合計値を返す
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.Sum(); // -> 116Count
要素数を返す
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.Count(); // -> 6Aggregate
アキュムレータ関数で処理した結果を返す
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.Aggregate((now, next) => now + next); //やっている事はSUMと同じ // -> 116
判定
All
全ての要素が条件を満たしているか判定
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.All(e => e > 10); // -> FalseAny
条件を満たす要素が含まれているか判定
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.Any(e => e > 10); // -> TrueContains
指定した要素が含まれているか判定
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.Contains(20); // -> FalseSequenceEqual
2つのシーケンスが等しいかどうか判定
var source = new[] { 11, 16, 52, 21, 8, 8 }; var output = source.SequenceEqual(new[] { 11, 16, 21, 52, 8, 8}); // -> False
集合
Union
指定したシーケンスとの和集合を返す
var first = new[] { 11, 16, 52, 21, 8, 8 }; var second = new[] { 16, 21, 20, 3 }; var output = first.Union(second); // -> {11, 16, 52, 21, 8, 20, 3}Except
指定したシーケンスとの差集合を返す
var first = new[] { 11, 16, 52, 21, 8, 8 }; var second = new[] { 16, 21, 20, 3 }; var output = first.Except(second); // -> {11, 52, 8}Intersect
指定したシーケンスとの積集合を返す
var first = new[] { 11, 16, 52, 21, 8, 8 }; var second = new[] { 16, 21, 20, 3 }; var output = first.Intersect(second); // -> {16, 21}
ソート
OrderBy
昇順にソートしたシーケンスを返す
var source = new[] { new { Name = "C#", Version = 7 }, new { Name = "C++", Version = 17 }, new { Name = "Java", Version = 9 }, new { Name = "PHP", Version = 7 }, }; var output = source.OrderBy(e => e.Version); // -> {{ Name = C#, Version = 7 }, // { Name = PHP, Version = 7 }, // { Name = Java, Version = 9 }, // { Name = C++, Version = 17 }}OrderByDescending
降順にソートしたシーケンスを返す
var source = new[] { new{ Name = "C#", Version = 7 }, new{ Name = "C++", Version = 17 }, new{ Name = "Java", Version = 9 }, new{ Name = "PHP", Version = 7 }, }; var output = source.OrderByDescending(e => e.Version); // -> {{ Name = C++, Version = 17 }, // { Name = Java, Version = 9 }, // { Name = C#, Version = 7 }, // { Name = PHP, Version = 7 }}ThenBy
ソートしたシーケンスに対し、キーが等しい要素同士を昇順にソートしたシーケンスを返す
var source = new[] { new{ Name = "C#", Version = 7 }, new{ Name = "C++", Version = 17 }, new{ Name = "Java", Version = 9 }, new{ Name = "PHP", Version = 7 }, }; var output = source.OrderBy(e => e.Version) .ThenBy(e => e.Name.Length); // -> {{ Name = C#, Version = 7 }, // { Name = PHP, Version = 7 }, // { Name = Java, Version = 9 }, // { Name = C++, Version = 17 }}ThenByDescending
ソートしたシーケンスに対し、キーが等しい要素同士を降順にソートしたシーケンスを返す
var source = new[] { new { Name = "C#", Version = 7 }, new { Name = "C++", Version = 17 }, new { Name = "Java", Version = 9 }, new { Name = "PHP", Version = 7 }, }; var output = source.OrderBy(e => e.Version) .ThenByDescending(e => e.Name.Length); // -> {{ Name = PHP, Version = 7 }, // { Name = C#, Version = 7 }, // { Name = Java, Version = 9 }, // { Name = C++, Version = 17 }}Reverse
逆順にソートしたシーケンスを返す
var source = new[] { new{Name = "C#", Version = 7}, new{Name = "C++", Version = 17}, new{Name = "Java", Version = 9}, new{Name = "PHP", Version = 7}, }; var output = source.Reverse(); // -> {{ Name = PHP, Version = 7 }, // { Name = Java, Version = 9 }, // { Name = C++, Version = 17 }, // { Name = C#, Version = 7 }}
射影
Select
1つの要素を単一の要素に射影する
var source = new[] { new { Name = "C#", Version = 7 }, new { Name = "C++", Version = 17 }, new { Name = "Java", Version = 9 }, new { Name = "PHP", Version = 7 }, }; var output = source.Select(e => e.Name); // -> {{ C#, C++, Java, PHP }SelectMany
1つの要素から複数の要素に射影し、その結果を1つのシーケンスとして返す
var source = new[] { new { Name = "C#", Version = 7 }, new { Name = "C++", Version = 17 }, new { Name = "Java", Version = 9 }, new { Name = "PHP", Version = 7 }, }; var output = source.SelectMany(e => e.Name); // -> {{ C, #, C, +, +, J, a, v, a, P, H, P }GroupBy
指定のキーで要素をグループ化し、そのキーとグループのシーケンスを返す
var source = new[] { new { Name = "C#", Version = 7 }, new { Name = "C++", Version = 17 }, new { Name = "Java", Version = 9 }, new { Name = "PHP", Version = 7 }, }; var output = source.GroupBy(e => e.Version); // -> { Key=7, Source={{ Name = C#, Version = 7 }, { Name = PHP, Version = 7 }}, // Key=17, Source={{ Name = C++, Version = 17}}, // Key=9, Source={{ Name = Java, Version = 9}}}
結合
Join
内部結合を行ったシーケンスを返す
var outer = new[] { new { Name = "C#", Version = 7 }, new { Name = "C++", Version = 17 }, new { Name = "Java", Version = 9 }, new { Name = "PHP", Version = 7 }, }; var inner = new[] { new { Name = "C#", DesignedBy = "Microsoft" }, new { Name = "Java", DesignedBy = "Sun Microsystems" }, new { Name = "Java", DesignedBy = "Oracle" }, }; var output = outer.Join(inner, o => o.Name, i => i.Name, (o, i) => new { o.Name, o.Version, i.DesignedBy}); // -> {{ Name = C#, Version = 7, DesignedBy = Microsoft }, // { Name = Java, Version = 9, DesignedBy = Sun Microsystems }, // { Name = Java, Version = 9, DesignedBy = Oracle }}GroupJoin
左部結合を行って指定のキーでグループ化し、そのキーとグループのシーケンスを返す
var outer = new[] { new { Name = "C#", Version = 7 }, new { Name = "C++", Version = 17 }, new { Name = "Java", Version = 9 }, new { Name = "PHP", Version = 7 }, }; var inner = new[] { new { Name = "C#", DesignedBy = "Microsoft" }, new { Name = "Java", DesignedBy = "Sun Microsystems" }, new { Name = "Java", DesignedBy = "Oracle" }, }; var output = outer.GroupJoin(inner, o => o.Name, i => i.Name, (o, i) => new { o.Name, o.Version, DesignedBy = i.Select(e => e.DesignedBy)}); // -> {{ Name = C#, Version = 7, DesigndBy = {Microsoft} }, // { Name = C++, Version = 17, DesigndBy = {} }, // { Name = Java, Version = 9, DesigndBy = {Sun Microsystems, Oracle} }, // { Name = PHP, Version = 7, DesigndBy = {} }}Concat
2つのシーケンスを連結する
var outer = new[] { new { Name = "C#", Version = 7 }, new { Name = "C++", Version = 17 }, new { Name = "Java", Version = 9 }, new { Name = "PHP", Version = 7 }, }; var outer2 = new[] { new { Name = "Swift", Version = 4 }, new { Name = "Visual Basic", Version = 6 }, }; var output = outer.Concat(outer2); // -> {{ Name = C#, Version = 7 }, // { Name = C++, Version = 17 }, // { Name = java, Version = 9 }, // { Name = PHP, Version = 7 }, // { Name = Swift, Version = 4}, // { Name = Visual Basic, Version = 6}}DefaultIfEmpty
シーケンスを返す
シーケンスが空の場合、既定値もしくは任意の要素を返すvar outer = new[] { new { Name = "C#", Version = 7 }, new { Name = "C++", Version = 17 }, new { Name = "Java", Version = 9 }, new { Name = "PHP", Version = 7 }, }; var output = outer.DefaultIfEmpty(); // -> {{ Name = C#, Version = 7 }, // { Name = C++, Version = 17 }, // { Name = java, Version = 9 }, // { Name = PHP, Version = 7 }}Zip
指定した関数で、2つのシーケンスを1つのシーケンスにマージする
var outer = new[] { new { Name = "C#", Version = 7 }, new { Name = "C++", Version = 17 }, new { Name = "Java", Version = 9 }, new { Name = "PHP", Version = 7 }, }; var outer2 = new[] { new { Name = "Swift", Version = 4 }, new { Name = "Visual Basic", Version = 6 }, }; var output = outer.Zip(outer2, (o1, o2) => o1.Name + "&" + o2.Name); // -> { C#&Swift, C++&VisualBasic }
- 投稿日:2019-10-21T14:17:21+09:00
今更聞けないモンテカルロ法(円周率編)
カジノを潰したという都市伝説を持つモンテカルロ法で、円周率を求めます。
C#ですけれど、ソースコードはこちらにあります。
https://github.com/ma3100/AlgorithmPrimer/blob/master/FirstAlgorithmForCSharp/First/MonteCalro.cs
※随時追加予定モンテカルロ法とは?
モンテカルロ法は、問題を数値計算で解くのではなく確率を利用して解く事です。
例えば今回テーマにしている円周率の場合、「円周の長さと直径の比」から一生懸命計算する訳ですが、モンテカルロ法は上記の計算を必要としません。
そのため、今回学ぶ方法で求める円周率は正確な値ではないのですが、導入としては適しているのでその辺りは割り切ってください。
モンテカルロ法で円周率を求める際の考え方
※半径が1の円で説明します。
- 円周率を求めたい円を四分割し、直径と同じ辺を持つ正方形で1/4の円を囲みます。
一様実数乱数であれば、「1/4の円の中に収まる乱数と全ての乱数」の比は、「1/4の円の面積と正方形の面積の比」と同じになります。
1/4の円の面積 : 正方形の面積 = 1/4の円の中に収まる乱数の数 : 全ての乱数の数
1/4の円の中に収まる乱数の数をa,円の外に出た乱数をbと置くと、
円周率/4 : 1 = a : a + b
円周率 = 4a / (a + b)
= 4a / n(発生した乱数の総数)実装(C#)
pymonte.csconst int RANDOMNUM = 10000; void GetMonteCalro() { double x, y, montePai; var random = new Random(); var inCircle = 0; foreach(var i in Enumerable.Range(0,RANDOMNUM)) { x = random.NextDouble(); y = random.NextDouble(); if(x * x + y * y <= 1) { inCircle++; } } montePai = (double)4 * inCircle / RANDOMNUM; Console.WriteLine($"Answer:{montePai}"); }※x^2 + y ^2 が半径を超えないのであれば、円の中にあると見なせます。
例えばRANDOMNUMの値を100などにすると、かなり怪しい結果になるのですがこの値を大きくしていくと、だんだんと3.14に近しい数になっていきます。
今回は以上です、次はモンテカルロ法を用いて円の面積を求めていきます。
- 投稿日:2019-10-21T13:42:25+09:00
Rust/tokioで非同期チャットサーバー
TL;DR
- Rust/tokioで非同期チャットサーバーを作ってみました。
- 比較のために、同期サーバー作ってみました。
- テストしてパフォーマンスを比較しました。
- 思った通り、非同期の方がCPU負荷が少なく、同じ処理を実行できました。
- ついでに、C#でも非同期サーバーを実装して、比較しました。
- Rustの方が速そうでした。
※若干説明が正確でないところがあります。正しい理解のためには、公式ドキュメントなど参照してください。
Rustとは
- GCなしで、安全なメモリ管理
- 実行時コストなしで、抽象化
- 関数型言語からの影響
「プログラミング言語「Rust」は、「C」や「C++」並みのパフォーマンスと、開発者が墓穴を掘るのを防ぐ仕組みとを兼ね備えたプログラミング言語だ」
「プログラミング言語「Rust」--2019年こそ学ぶべき7つの理由」
https://japan.techrepublic.com/article/35131764.htmTokioとは
スレッドプールを使って、タスクを並列にいい感じに実行してくれるやつです。
※半年ほど前に作ったので、std::futureが入る前の古い方のtokioです。
tokio = "0.1.20"
futures = "0.1.25"
Tokioを使って非同期なチャットサーバーを作ってみよう
設計はこんな感じ。
送受信と入室処理がそれぞれ別タスク(別スレッド)で動く。
Channelとは、スレッドセーフなキューだと思ってもらえればOKです。
比較のために、同期のチャットも作ってみる
設計はこんな感じ。
送受信も入室処理も全部同じタスクで処理。
何もすることがない時は、ループがぐるぐる回る。
パフォーマンス測定
- 50人が同時にアクセス
- サーバーは、1部屋10人ずつでルームを作る
- CLはそれぞれ、10〜100ミリ秒ずつランダムに間を開けながら、20〜200バイトのデータを繰り返し送信
結果:同期の場合
CPU使用率が上がったり下がったり。誰も喋っていない時、ループをぐるぐる回っているので、その間CPUが100%になっている
結果:非同期の場合
いい感じにCPU使用率が安定している。計画どおり ( ̄ー ̄)ニヤリ
C#で非同期もやってみた
CPU使用率はRustと同じくらい。でも、ネットワークの速度がRustより半分くらい遅い。
やっぱりRustは速い!ということに違いない。わかったこと
- 非同期の方がCPUを効率よく使える(あたりまえですが・・)
- C#よりRustで書いた方が速度が出る
C#は async/awaitが使えるので非同期も簡単に書ける。
Rustももうすぐ async/awaitが来るので期待大。Rustを使って良かったところ
- トレイトを使ってソケットの送受信の機能を抽象化。Room側のソースはTCP/WebSocket を意識しないで書ける。しかも抽象化による実行時コストがゼロ。
- Futureトレイトを使うことで、タスクが書きやすい(でも async/await があればもっと書きやすくなるはず)
Rustを使ってつらかったところ
- Future使った時に型推論がどう動くのかが理解できずに苦しんだ
- ドキュメントがあまり整備されてない
- サンプルが少ない
今回つかったソース
サーバー:
https://github.com/mas-yo/rustgs/tree/lt20190605
テストクライアント:
https://github.com/mas-yo/rustgs-testcl/tree/lt20190605
- 投稿日:2019-10-21T13:42:25+09:00
Rustで非同期チャットサーバー
TL;DR
- Rustで非同期チャットサーバーを作ってみました。
- 比較のために、同期サーバー作ってみました。
- テストしてパフォーマンスを比較しました。
- 思った通り、非同期の方がCPU負荷が少なく、同じ処理を実行できました。
- ついでに、C#でも非同期サーバーを実装して、比較しました。
- Rustの方が速そうでした。
※若干説明が正確でないところがあります。正しい理解のためには、公式ドキュメントなど参照してください。
Rustとは
- GCなしで、安全なメモリ管理
- 実行時コストなしで、抽象化
- 関数型言語からの影響
「プログラミング言語「Rust」は、「C」や「C++」並みのパフォーマンスと、開発者が墓穴を掘るのを防ぐ仕組みとを兼ね備えたプログラミング言語だ」
「プログラミング言語「Rust」--2019年こそ学ぶべき7つの理由」
https://japan.techrepublic.com/article/35131764.htmTokioとは
スレッドプールを使って、タスクを並列にいい感じに実行してくれるやつです。
※半年ほど前に作ったので、std::futureが入る前の古い方のtokioです。
tokio = "0.1.20"
futures = "0.1.25"
Tokioを使って非同期なチャットサーバーを作ってみよう
設計はこんな感じ。
送受信と入室処理がそれぞれ別タスク(別スレッド)で動く。
Channelとは、スレッドセーフなキューだと思ってもらえればOKです。
比較のために、同期のチャットも作ってみる
設計はこんな感じ。
送受信も入室処理も全部同じタスクで処理。
何もすることがない時は、ループがぐるぐる回る。
パフォーマンス測定
- 50人が同時にアクセス
- サーバーは、1部屋10人ずつでルームを作る
- CLはそれぞれ、10〜100ミリ秒ずつランダムに間を開けながら、20〜200バイトのデータを繰り返し送信
結果:同期の場合
CPU使用率が上がったり下がったり。誰も喋っていない時、ループをぐるぐる回っているので、その間CPUが100%になっている
結果:非同期の場合
いい感じにCPU使用率が安定している。計画どおり ( ̄ー ̄)ニヤリ
C#で非同期もやってみた
CPU使用率はRustと同じくらい。でも、ネットワークの速度がRustより半分くらい遅い。
やっぱりRustは速い!ということに違いない。わかったこと
- 非同期の方がCPUを効率よく使える(あたりまえですが・・)
- C#よりRustで書いた方が速度が出る
C#は async/awaitが使えるので非同期も簡単に書ける。
Rustももうすぐ async/awaitが来るので期待大。Rustを使って良かったところ
- トレイトを使ってソケットの送受信の機能を抽象化。Room側のソースはTCP/WebSocket を意識しないで書ける。しかも抽象化による実行時コストがゼロ。
- Futureトレイトを使うことで、タスクが書きやすい(でも async/await があればもっと書きやすくなるはず)
Rustを使ってつらかったところ
- Future使った時に型推論がどう動くのかが理解できずに苦しんだ
- ドキュメントがあまり整備されてない
- サンプルが少ない
今回つかったソース
サーバー:
https://github.com/mas-yo/rustgs/tree/lt20190605
テストクライアント:
https://github.com/mas-yo/rustgs-testcl/tree/lt20190605
- 投稿日:2019-10-21T11:19:14+09:00
C#で、文字と図形からなるWindowsアイコン(.Ico)【透過】を作成するプログラムを作ってみた
やれること
こんな感じのWindowsアイコンが作れる。
UIを作りこむのが大変なので、下記のソースコード(EasyDrawIcon.cs
)を直接修正して使う方向で。注意事項
作ったアイコンについては著作権に注意してください。特にフォントには著作権があります。
どのフォントがセーフなのかは自分はわかりません。ソースコード
以前のコードをちょっとリファクタリングして、アイコンデータ作成部分を
IconUtility.cs
として分離した。
ソースコード - IconUtility.cs
IconUtility.csusing System; using System.Collections; using System.Collections.Generic; using System.Data; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; using System.Reflection; using System.Runtime.InteropServices; namespace IconUtility { public class Icons { class IconEntry { [StructLayout(LayoutKind.Sequential)] public struct IconDir { public short icoReserved; // must be 0 public short icoResourceType; //must be 1 for icon public short icoResourceCount; public IconDir(int n) { icoReserved = 0; icoResourceType = 1; icoResourceCount = (short)n; } } [StructLayout(LayoutKind.Sequential)] public struct IconDirEntry { byte _Width; byte _Height; public byte ColorCount; public byte reserved1; public short reserved2; public short reserved3; public int icoDIBSize; public int icoDIBOffset; public int Width{get{return (_Width>0)?_Width:256;}} public int Height{get{return (_Height>0)?_Height:256;}} public IconDirEntry(int w, int h) { if ( w<0 || w>256 || h<0 || h>256 ) { throw new Exception("Size parameter error"); } _Width = (byte)w; _Height = (byte)h; ColorCount=0; reserved1=0; reserved2=0; reserved3=0; icoDIBSize=0; icoDIBOffset=0; } } [StructLayout(LayoutKind.Sequential)] struct BitmapInfoHeader { public int biSize; // must be 40 public int biWidth; public int biHeight; public short biPlanes; // must be 1 public short biBitCount; // color public int biCompression; // 0:not compress public int biSizeImage; public int biXPixPerMeter; public int biYPixPerMeter; public int biClrUsed; public int biClrImportant; public BitmapInfoHeader(int w, int h) { biSize = 40; biWidth = w; biHeight = h*2; // 本体とmaskを含むため2倍とする決まりらしい biPlanes = 1; biBitCount = 32; biCompression=0; biSizeImage=0; biXPixPerMeter=0; biYPixPerMeter=0; biClrUsed=0; biClrImportant=0; } } IconDirEntry iconDirEntry; BitmapInfoHeader bitmapInfoHeader; byte[] bitmapBody; byte[] bitmapMask; public System.Drawing.Size Size{get{return new System.Drawing.Size(iconDirEntry.Width, iconDirEntry.Height);}} public int Width{get{return iconDirEntry.Width;}} public int Height{get{return iconDirEntry.Height;}} int BitPerPixel{get{return bitmapInfoHeader.biBitCount;}} int CalcDIBSize() { return Marshal.SizeOf(typeof(BitmapInfoHeader)) + bitmapBody.Length + bitmapMask.Length; } public int UpdateIconDirEntry(int icoDIBOffset) { iconDirEntry.icoDIBOffset = icoDIBOffset; iconDirEntry.icoDIBSize = CalcDIBSize(); return iconDirEntry.icoDIBSize; } public void WriteIconDirEntryTo(BinaryWriter writer) { Icons.CopyDataToByteArray<IconDirEntry>(writer, iconDirEntry); } public void WriteDataTo(BinaryWriter writer) { Icons.CopyDataToByteArray<BitmapInfoHeader>(writer, bitmapInfoHeader); writer.Write(bitmapBody); writer.Write(bitmapMask); } IconEntry(IconDirEntry _iconDirEntry, BitmapInfoHeader _bitmapInfoHeader, byte[] _bitmapBody, byte[] _bitmapMask) { iconDirEntry = _iconDirEntry; bitmapInfoHeader = _bitmapInfoHeader; bitmapBody = _bitmapBody; bitmapMask = _bitmapMask; } // 本体 public IconEntry(Bitmap bmp, Color? alphaColor) { int w = bmp.Width; int h = bmp.Height; if ( w>256 || h>256 ) { throw new Exception("size parameter error"); } iconDirEntry = new IconDirEntry(w, h); bitmapInfoHeader = new BitmapInfoHeader(w, h); bitmapBody = new byte[Icons.GetBitmapBodySize(w, h, bitmapInfoHeader.biBitCount)]; bitmapMask = new byte[Icons.GetBitmapMaskSize(w, h)]; Draw32bppBitmapToData(bmp, alphaColor); } void Draw32bppBitmapToData(Bitmap bmp, Color? alphaColor) { Array.Clear(bitmapMask, 0, bitmapMask.Length); Array.Clear(bitmapBody, 0, bitmapBody.Length); BitmapData bd = bmp.LockBits(new Rectangle(0,0,bmp.Width,bmp.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); try { IntPtr ptr = bd.Scan0; byte[] pixels = new byte[bd.Stride * bmp.Height]; Marshal.Copy(ptr, pixels, 0, pixels.Length); int maskStride = (((Width+7)/8+3)/4)*4; int icoStride = Width*4; for (int y = 0; y < bd.Height; y++) { for (int x = 0; x < bd.Width; x++) { int posIco = y * icoStride + 4*x; int bytePosMask = y * maskStride + x/8; int bitPosMask = 7-(x%8); int pos = (bd.Height-1-y) * bd.Stride + x * 4; bitmapBody[posIco ] = pixels[pos]; //blue; bitmapBody[posIco+1] = pixels[pos+1]; //green; bitmapBody[posIco+2] = pixels[pos+2]; //red; bitmapBody[posIco+3] = pixels[pos+3]; //alpha if ( pixels[pos+3] == 0 || (alphaColor != null && pixels[pos] ==alphaColor.Value.B && pixels[pos+1]==alphaColor.Value.G && pixels[pos+2]==alphaColor.Value.R )) { //bitmapMask[bytePosMask] |= (byte)(1<<bitPosMask); // 32bit色のiconだとmaskではなくalpha channelが使用されるっぽい bitmapBody[posIco+3] = 0x00; } } } } finally { bmp.UnlockBits(bd); } } } int UpdateIconDirEntries() { iconDir.icoResourceCount = (short)iconEntries.Count; int offset = Marshal.SizeOf(typeof(IconEntry.IconDir)) + iconEntries.Count * Marshal.SizeOf(typeof(IconEntry.IconDirEntry)); for (int i=0;i<iconEntries.Count;i++) { offset += iconEntries[i].UpdateIconDirEntry(offset); } return offset; } static int GetBitmapBodySize(int w, int h, int bitCount) { return ((((w*bitCount + 7)/8)+3)/4)*4 * h; } static int GetBitmapMaskSize(int w, int h) { return ((((w+7)/8)+3)/4)*4 * h; } static TStruct CopyDataToStruct<TStruct> (BinaryReader reader) where TStruct : struct { var size = Marshal.SizeOf(typeof(TStruct)); var ptr = IntPtr.Zero; try { ptr = Marshal.AllocHGlobal(size); Marshal.Copy(reader.ReadBytes(size), 0, ptr, size); return (TStruct)Marshal.PtrToStructure(ptr, typeof(TStruct)); } finally { if (ptr != IntPtr.Zero) { Marshal.FreeHGlobal(ptr); } } } static void CopyDataToByteArray<TStruct>(BinaryWriter writer, TStruct s) where TStruct : struct { var size = Marshal.SizeOf(typeof(TStruct)); var buffer = new byte[size]; var ptr = IntPtr.Zero; try { ptr = Marshal.AllocHGlobal(size); Marshal.StructureToPtr(s, ptr, false); Marshal.Copy(ptr, buffer, 0, size); } finally { if (ptr != IntPtr.Zero) { Marshal.FreeHGlobal(ptr); } } writer.Write(buffer); } IconEntry.IconDir iconDir; List<IconEntry> iconEntries; // ------------------------------------------------------------------------------ // public members public int Count {get{return iconEntries.Count;}} public Icons() { iconDir = new IconEntry.IconDir(0); iconEntries = new List<IconEntry>(); } public void AddIcon(Bitmap bmp, Color alphaColor) { iconEntries.Add(new IconEntry(bmp, alphaColor)); UpdateIconDirEntries(); } public void AddIcon(Bitmap bmp) { iconEntries.Add(new IconEntry(bmp, null)); UpdateIconDirEntries(); } public bool SaveToFile(string path) { //int size = UpdateIconDirEntries(); using ( var fs = new FileStream(path, FileMode.Create) ) { using ( var writer = new BinaryWriter(fs) ) { CopyDataToByteArray<IconEntry.IconDir>(writer, iconDir); foreach(var t in iconEntries) { t.WriteIconDirEntryTo(writer); } foreach(var t in iconEntries) { t.WriteDataTo(writer); } } } return true; } } }
ソースコード - EasyDrawIcon.cs
EasyDrawIcon.csusing System; using System.Collections; using System.Collections.Generic; using System.Data; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.IO; using System.Reflection; using System.Runtime.InteropServices; using System.Windows.Forms; using IconUtility; class EasyDraw:Form { PictureBox pct; const int MAX_SIZE = 64; const int CANVAS_SIZE = 512; // const int ZOOM = 2; Font curFont; int curFontStyle; Pen curPen; Brush curBrush; PointF curPoint; GraphicsPath curPath; Graphics curG; const string DefaultFontName = "メイリオ"; // フォントを変える場合はここを変更 EasyDraw() { curPen = Pens.Black; curBrush = Brushes.White; curPath = new GraphicsPath(); curFont = new Font(DefaultFontName, 30.0f); // フォントサイズを変える場合はここを変更 if (curFont.Name != DefaultFontName) { MessageBox.Show("フォント \""+DefaultFontName+"\"の取得に失敗しました。\r\n" + "代わりにフォント\"" + curFont.Name + "\"が使用されます。\r\n"); } MessageBox.Show("アイコンに文字を使う場合は、フォントの著作権に注意してください。"); curFontStyle = (int)FontStyle.Regular; // Bold , Italic , Strikeout , Underline の bitOR を設定可能 pct = new PictureBox(); pct.Image = new Bitmap(CANVAS_SIZE, CANVAS_SIZE); pct.Dock = DockStyle.Fill; Controls.Add(pct); ClientSize = new Size(CANVAS_SIZE, CANVAS_SIZE); Load+=(sender,e)=>{Draw();}; } void moveto(float x, float y) { curPoint.X = x; curPoint.Y = y; } void rmoveto(float rx, float ry) { curPoint.X += rx; curPoint.Y += ry; } void lineto(float x, float y) { curPath.AddLine(curPoint.X, curPoint.Y, x, y); curPoint.X = x; curPoint.Y = y; } void rlineto(float rx, float ry) { curPath.AddLine(curPoint.X, curPoint.Y, curPoint.X+rx, curPoint.Y+ry); curPoint.X += rx; curPoint.Y += ry; } void charpath(string s) { var sf = new StringFormat(); sf.Alignment = StringAlignment.Center; // 横方向の中央 sf.LineAlignment = StringAlignment.Center; // 縦方向の中央 FontFamily ff = curFont.FontFamily; curPath.AddString(s, ff, curFontStyle, curFont.Size, curPoint, sf); } void newpath() { curPath.Reset(); curPath.StartFigure(); } void closepath() { curPath.CloseFigure(); } void stroke() { if ( curG != null ) { curG.DrawPath(curPen, curPath); } } void fill() { if ( curG != null ) { curPath.FillMode = FillMode.Winding; curG.FillPath(curBrush, curPath); } } void eofill() { if ( curG != null ) { curPath.FillMode = FillMode.Alternate; curG.FillPath(curBrush, curPath); } } float cos(float degree) { return (float)Math.Cos((degree/180.0)*Math.PI); } float sin(float degree) { return (float)Math.Sin((degree/180.0)*Math.PI); } void Draw() { Bitmap bmp = CreateTransparentBitmap(MAX_SIZE, MAX_SIZE); curG = Graphics.FromImage(bmp); curG.SmoothingMode = SmoothingMode.AntiAlias; try { // ---- ここの中を変更すればよい curBrush = new LinearGradientBrush(new Point(0, 0), new Point(64, 64), Color.Red, Color.Yellow); newpath(); for ( int deg=0 ; deg<360 ; deg+=60 ) { float x = 32+31*cos(deg); float y = 32+31*sin(deg); if ( deg == 0 ) { moveto(x,y); } else { lineto(x,y); } } closepath(); fill(); curBrush = new LinearGradientBrush(new Point(0, 0), new Point(64, 64), Color.Blue, Color.Black); newpath(); moveto(32,36); // (32,32)が中央だが、文字列描画位置がずれるので微調整 charpath("C#"); closepath(); fill(); // ---- } finally { curG.Dispose(); curG = null; } int zoom = 2; Graphics g = Graphics.FromImage(pct.Image); g.FillRectangle(Brushes.White, 0, 0, pct.Image.Width, pct.Image.Height); g.DrawImage(bmp, 0, 0, bmp.Width*zoom, bmp.Height*zoom); g.Dispose(); Icons icons; icons = new Icons(); icons.AddIcon(bmp); icons.SaveToFile("Output.ico"); } // 透過色で初期化 Bitmap CreateTransparentBitmap(int width, int height) { Bitmap bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb); BitmapData bd = bmp.LockBits(new Rectangle(0,0,width,height), ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb); try { unsafe { // 書き込み byte* ptr = (byte*)bd.Scan0; for ( int y=0 ; y<height ; y++ ) { for ( int x=0 ; x<width ; x++ ) { ptr[y*bd.Stride + 4*x ] = 0;// B ptr[y*bd.Stride + 4*x + 1] = 0;// G ptr[y*bd.Stride + 4*x + 2] = 0;// R ptr[y*bd.Stride + 4*x + 3] = 0;// alpha = 0 (透過) } } } } finally { bmp.UnlockBits(bd); } return bmp; } [STAThread] static void Main() { Application.Run(new EasyDraw()); } }メソッド名について
PostScript言語をまねてみた。
コンパイル方法
csc /unsafe EasyDrawIcon.cs IconUtility.cs
アイコンのキャッシュを更新する方法
同じファイル名のアイコンをつくると、アイコンの表示が更新されない。
コマンドプロンプトなどで
Windows 10では「ie4uinit.exe -show
」、
それ以外のバージョンでは「ie4uinit.exe -ClearIconCache
」
を実行すればよい。参考サイト: https://news.mynavi.jp/article/20180131-windows_icon/
- 投稿日:2019-10-21T08:04:21+09:00
ハードエッジでも縁崩れしない理想的なアウトラインシェーダを作ってみた
はじめに
仕事でUnityゲーム開発しているyoship1639です。
ハードエッジでもこの様に縁崩れせずにアウトラインを奇麗に出すシェーダを思いついたので公開します。縁取りをするシェーダは多々ありますが、特定のパターンで上手くいかない場合があります。
それぞれ代表的な既存のアウトラインシェーダの特徴と、上手くいかないパターンは以下の通りです。
モデルを拡大する手法
特徴:一般的な手法。1パス目で拡大したモデルをアウトライン色で描画し、2パス目で通常描画する
ダメなパターン:ハードエッジモデルの場合やモデルの中心(各頂点の中心)が0じゃない場合に破綻するステンシルバッファを使う手法
特徴:これも一般的な手法。1パス目で拡大したモデルをステンシルバッファに描画し、2パス目で通常描画する。ポストプロセス等でアウトラインに色を付ける
ダメなパターン:モデルを拡大する手法と同様法線を使う手法
特徴:カメラ視点から見てモデルの法線がほぼ垂直になる(内積がほぼ0になる)ピクセルをアウトライン色にする手法
ダメなパターン:アウトラインが超絶汚い深度バッファを使う手法
特徴:深度バッファを使って深度が急変する箇所をアウトラインとみなす手法。
ダメなパターン:シーン全体にアウトラインがかかってしまう各手法の細かい説明等はこちらを参考にしてください
【Unity】【シェーダ】4種のアウトライン描画方法とその特徴と、上記の様に一長一短があります。
理想的なのは、特定のハードエッジなモデルでもきれいなアウトラインを表示することです。
そこで、上記のダメなパターンを払拭したアウトラインシェーダを思いついたので解説したいと思います。アルゴリズム解説
今回思いついたのは、GrabPassを用いたアウトラインシェーダです。
GrabPassは簡単に説明すると、直前までのパスの描画結果を背景含めてテクスチャとして出力することです。Grabテクスチャはその描画結果のテクスチャを指します。アルゴリズム概要は以下の通りです。
- 【1パス目】モデルをシーン上で絶対使わないような色(ダミー色)でソリッド描画する
- 1の結果をGrabPassとして2パス目に渡す
- 【2パス目】ピクセルシェーダでGrabテクスチャのUV値付近をアウトライン幅で一定数サンプリングする
- サンプリングの結果ダミー色が含まれていなかった場合、アウトライン付近と判断しアウトライン色で描画する。それ以外は通常描画する。
上記のアルゴリズムでモデルを描画すると、ハードエッジであってもきれいにアウトラインを描画することができます。
それぞれ説明します。
1. 【1パス目】でモデルをシーン上で絶対使わないような色(ダミー色)でソリッド描画する
まず、1パス目でアウトライン描画したいモデルをシーン上で絶対使わないような色でモデルを膨らませずに塗りつぶし描画します。なぜシーン上で絶対使わないような色でないといけないかというと、Grabテクスチャの背景と被る色だとアウトラインが破綻する可能性があるからです。この色をダミー色と呼んでおきます。アウトライン色ではありませんのでご注意。
使いやすいダミー色は
rgb(255,0,255)
なので、この色を使います。Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { half4 vertex : POSITION; }; struct v2f { half4 vertex : SV_POSITION; }; v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); return o; } fixed4 frag(v2f i) : SV_Target { return fixed4(1.0, 0.0, 1.0, 0); } ENDCG }2. 1の結果をGrabPassとして2パス目に渡す
これは特に何も考えずにシェーダコード内で
GrabPass {}
と書けば大丈夫です。これで1パス目の結果をテクスチャとして2パス目に渡すことができます。3. 【2パス目】ピクセルシェーダでGrabテクスチャのUV値付近をアウトライン幅で一定数サンプリングする
ここが今回の手法のミソです。
頂点シェーダはいつもの感じに行いますが、ピクセルシェーダは少し違います。まず、GrabPassで入手したGrabテクスチャのピクセルのUV値を算出します。これは頂点シェーダで
ComputeGrabScreenPos
を呼べばUnityが勝手に算出してくれます。このUV値のサンプリング結果はモデルのピクセル値と一致します。(つまりrgb(255,0,255)
です)UV値を算出したら、そのUV値の付近をアウトライン幅分ずらして一定数サンプリングします。一定数というのはアウトラインと判定するのに十分な数です。今回の手法は6方向分サンプリングすれば十分です。
#define SAMPLE_NUM 6 #define SAMPLE_INV 0.16666666 #define PI2 6.2831852 #define EPSILON 0.001 #define DUMMY_COLOR fixed3(1.0, 0.0, 1.0) sampler2D _GrabTexture; half _OutlineWidth; v2f vert(appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.grabPos = ComputeGrabScreenPos(o.pos); return o; } fixed4 frag(v2f i) : SV_Target { // サンプリングのオフセット(アウトラインの幅) half2 delta = (1 / _ScreenParams.xy) * _OutlineWidth; int edge = 0; [unroll] for (int j = 0; j < SAMPLE_NUM && edge == 0; j++) { // オフセット分ずらしてサンプリング fixed4 tex = tex2D(_GrabTexture, i.grabPos.xy / i.grabPos.w + half2(sin(SAMPLE_INV * j * PI2) * delta.x, cos(SAMPLE_INV * j * PI2) * delta.y)); // ダミー色と同でないならアウトラインであると判定 edge += distance(tex.rgb, DUMMY_COLOR) < EPSILON ? 0 : 1; } ...4. サンプリングの結果ダミー色が含まれていなかった場合、アウトライン付近と判断しアウトライン色で描画する。それ以外は通常描画する。
サンプリングした結果、ダミー色しか含まれていない場合はアウトライン付近ではないと判断し通常のモデル描画を行います。
ダミー色以外が1つ以上含まれていた場合は、アウトライン付近であると判断できるので、アウトライン色で塗りつぶします。
これを実現すると、ハードエッジであっても関係なく、かつ特定のモデルのみアウトラインを適用させることができます。シェーダコード
今回作成したアウトラインシェーダのシェーダコード全文です。コピペすれば動きます。
必要最小限のコードしか書いていないので、ライト処理やシャドー処理などは行いません。Shader "Custom/Outline" { Properties { _MainColor("Main Color", Color) = (1, 1, 1, 1) _MainTex("Texture", 2D) = "white" {} _OutlineColor("Outline Color", Color) = (0.0, 0.0, 0.0, 1) [Slider(0.1)] _OutlineWidth("Outline Width", Range(0.0, 10.0)) = 3 } SubShader { Tags { "RenderType" = "Opaque" } LOD 100 // 【1パス目】ダミー色で塗りつぶし Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { half4 vertex : POSITION; }; struct v2f { half4 vertex : SV_POSITION; }; v2f vert(appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); return o; } fixed4 frag(v2f i) : SV_Target { return fixed4(1.0, 0.0, 1.0, 0); } ENDCG } GrabPass {} // 【2パス目】Grabテクスチャを使ってアウトライン+通常描画 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" #define SAMPLE_NUM 6 #define SAMPLE_INV 0.16666666 #define PI2 6.2831852 #define EPSILON 0.001 #define DUMMY_COLOR fixed3(1.0, 0.0, 1.0) struct appdata { half4 vertex : POSITION; half2 uv : TEXCOORD0; }; struct v2f { half4 pos : SV_POSITION; half2 uv : TEXCOORD0; half4 grabPos : TEXCOORD1; }; sampler2D _GrabTexture; fixed4 _MainColor; sampler2D _MainTex; half4 _MainTex_ST; fixed4 _OutlineColor; half _OutlineWidth; v2f vert(appdata v) { v2f o; o.pos = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); o.grabPos = ComputeGrabScreenPos(o.pos); return o; } fixed4 frag(v2f i) : SV_Target { half2 delta = (1 / _ScreenParams.xy) * _OutlineWidth; int edge = 0; [unroll] for (int j = 0; j < SAMPLE_NUM && edge == 0; j++) { fixed4 tex = tex2D(_GrabTexture, i.grabPos.xy / i.grabPos.w + half2(sin(SAMPLE_INV * j * PI2) * delta.x, cos(SAMPLE_INV * j * PI2) * delta.y)); edge += distance(tex.rgb, DUMMY_COLOR) < EPSILON ? 0 : 1; } fixed4 col = lerp(tex2D(_MainTex, i.uv) * _MainColor, _OutlineColor, edge); return col; } ENDCG } } }おわりに
上記のシェーダコードはアウトラインを表示するための必要最小限の機能しか実装していません。しかし、アウトライン部分以外はいくらでも追加実装することができるので汎用性は高いのではないかと思います。
また、良い感じに描画してくれるアウトラインシェーダですが弱点があります。それはアウトラインシェーダを適用させたモデルを重ねた時です。この場合、GrabPassがモデルと背景の境界を正しく判定できなくなるので、アウトラインが多少破綻してしまいます。そのため、ダミー色をマテリアルごとに変えたりといった工夫が必要になるかもしれません。それ以外の場面では現状問題なく描画できます。
その他不具合があったらご連絡ください。改良版を考えます。
良きUnityライフを。
- 投稿日:2019-10-21T03:48:49+09:00
初心者がC#でグーグルスピーカーを動かす(備忘録:挫折編、Javescriptで書く)
これは挫折したところまで書いた話です。
google homeでは下記の通りにデータの受け渡しがされてます。引用:https://qiita.com/kenz_firespeed/items/0979ceb05e4e3299f313
Dialogfrowではbotの作成とかも出来て非常に面白そうなので
時期をみてやりたいなと思ってます。ただ、今回はC#で書きたいので
もっと簡単にコード書いて、グーグルホームにお話させたいな
と思いました。そこで見つけたのが、Node.jsで書く方法
ざっくり言うと、google-home-notifierというNode.jsのライブラリがあって
それをポチッとするとJavascriptで書くことが出来ます。google-home-notifierでJavascriptを書く方法を残しておきます。
まず、Node.jsを有効にする。
https://nodejs.org/ja/でもいいけど
https://qiita.com/taketakekaho/items/dd08cf01b4fe86b2e218
この記事が非常に参考になりました。
ただ、初心者には、bashの使い方に要注意。次にNode.jsの環境が整ったら、
npmを入れて、いざgoogle-home-notifierを導入へ
https://qiita.com/SatoTakumi/items/c9de7ff27e5b70508066
上記を参考にしました。$ # package.jsonを作ります
$ npm init$ # google-home-notifierを入れます
$ npm install google-home-notifier$ # yarn の方は下記のようにします。
$ yarn add google-home-notifierこのようにgoogle-home-notifier導入
その後、
Javascriptでファイルを作ります。
xxxx.js
とかのNode.jsのファイルを作ってとりあえず、サンプルコードを作ります。
jconst googlehome = require('google-home-notifier')
const language = 'ja';googlehome.device('Google-Home', language);
googlehome.notify('こんにちは。私はグーグルホームです。', function(res) {
console.log(res);
});こんな感じ。
さて、コンソールでこのファイルを動かして見ると、
喋るーーーーーーーと言うことで、Jacascriptでかけました。
C#ではどうするんだ?
と言うことで、次回は、C#の中にJavascriptを食い込ませたやり方を
考えたいと思います。まだまだ初心者なので、
配列を改めて勉強してるレベル。。。精進します。
参考にしたサイト
https://smarthacks.jp/mag/37615
https://qiita.com/kenz_firespeed/items/0979ceb05e4e3299f313
https://blog.okazuki.jp/entry/2018/06/28/141410
https://azure.microsoft.com/ja-jp/services/functions/最近学んだ言葉。
サーバーレス・コンピューティング
何かをWeb上で運用するとき、本来ならサーバーの管理が必要だが
サーバーを管理しなくていいものが作れるオーケストレーション
オーケストラのように指揮者がいて、楽器を演奏する人がいる
指揮者を管理、開発者に例えて、サーバーなり、コードなり諸々を指揮することイベント ドリブン型
イベントの発生をきっかけとして処理を始めることデプロイ
ビルドしてできた実行ファイルを、実行する環境に合わせて、実際に実行できるようにするトリガー
きっかけになる出来事が起こったら自動的に特定の処理を起動するソフトウェアの仕組みバインド
何らかの要素やデータ、ファイルなどが相互に結び付けられることfunctions
複雑なオーケストレーションの問題も解決できる、イベント ドリブン型のサーバーレス コンピューティング プラットフォームである Functions を使用して開発を効率化しましょう。追加のセットアップを必要としないローカルでのビルドとデバッグ、クラウドでの大規模なデプロイと運用、トリガーとバインドを使用したサービスの統合が可能です
要するに、サーバーレスで、簡単にものが作れまっせってこと。
楽チンにサービス作れますよってことJson
配列の()の中のものがkey
配列の箱の中にはなんでも(データ型は決まってるが)入るから
keyに対応させた表を作ること
簡単に言うと配列を表にしちゃえ!もっといえば、それをまとめたものJsonってのを作っておけば、引っ張ってくるとき楽やねん