20211127のC#に関する記事は6件です。

[C#] [WPF] ピアノのユーザーコントロールを作ってみる

作るもの 最小限のピアノを作る - 鍵盤の画像が表示される - 鍵盤にカーソルを当てると色が変わる - 鍵盤をクリックすると音が鳴る 準備 IDE: Microsoft Visual Studio Community 2019 「新しいプロジェクト」から「WPFアプリ(.NET Framework)」を選択してプロジェクトを作成 作成したプロジェクトに「ページ(WPF)」として「DemoPage.xaml」を追加 作成したプロジェクトに「ユーザーコントロール(WPF)」として「PianoKeyboardControl.xaml」を追加 PianoKeyboardControl.xaml を編集 PianoKeyboardControl.xaml <UserControl x:Class="<プロジェクトの名前空間>.PianoKeyboardControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:Mth.WPF.Piano.ForQiita" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800"> <Canvas x:Name="canvas"/> </UserControl> DemoPage.xaml を編集 DemoPage.xaml <Page x:Class="<プロジェクトの名前空間>.DemoPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:local="clr-namespace:Mth.WPF.Piano.ForQiita" mc:Ignorable="d" d:DesignHeight="450" d:DesignWidth="800" Background="Maroon" Title="DemoPage"> <Grid> <ScrollViewer VerticalScrollBarVisibility="Hidden" HorizontalScrollBarVisibility="Auto"> <local:PianoKeyboardControl x:Name="keyboard" /> </ScrollViewer> </Grid> </Page> 鍵盤の描画部分を作成する 鍵盤の形に関する Enum を用意する 鍵盤の色に関するもの(後から鍵盤の色を変えられるようにするため、白/黒ではなく、広い/狭いで表現) KeyType.cs enum KeyType { Wide, Narrow } 白い鍵盤の形状に関するもの(左右に黒い鍵盤のための切り欠きがあるかのフラグ) CutOffType.cs [Flags] enum CutOffType { None = 0, CutLeft = 1, CutRight = 2, CutBoth = CutLeft | CutRight } PianoKeyboardControl.canvas に鍵盤の形状に関する値を定義する 後から変更しやすいように諸々のパラメータをプロパティとしておく PianoKeyboardControl.cs // 88鍵ピアノの最低音(=A0)のノートナンバー private const byte GENERAL_PIANO_LOWEST_PITCH = 21; // 88鍵ピアノの最高音(=C8)のノートナンバー private const byte GENERAL_PIANO_HIGHEST_PITCH = 108; // 描画する最低音 public byte Lowest => GENERAL_PIANO_LOWEST_PITCH; // 描画する最高音 public byte Highest => GENERAL_PIANO_HIGHEST_PITCH; // 白鍵の幅 public int WideKeyWidth => 44; // 黒鍵の幅 public int NarrowKeyWidth => 28; // 白鍵の高さ public int WideKeyHeight => 220; // 黒鍵の高さ public int NarrowKeyHeight => 150; PianoKeyboardControl.canvas に鍵盤の描画オブジェクトを追加する PianoKeyboardControl.cs public PianoKeyboardControl() { InitializeComponent(); // オブジェクトを作成する CreateKeyboard(); } private void CreateKeyboard(byte lowest = GENERAL_PIANO_LOWEST_PITCH, byte highest = GENERAL_PIANO_HIGHEST_PITCH) { canvas.Children.Clear(); // curX = その時点で描画されている白鍵の右端 int curX = 0; // 一番左の鍵盤には左側の切り欠きがない curX = AddKey(curX, lowest, CutOffType.CutLeft); for (byte pitch = (byte) (lowest + 1); pitch < highest; ++pitch) { curX = AddKey(curX, pitch, CutOffType.None); } // 一番右の鍵盤には右側の切り欠きがない curX = AddKey(curX, highest, CutOffType.CutRight); // canvas の大きさを中身に合わせる canvas.Width = curX; canvas.Height = WideKeyHeight; } private int AddKey(int curX, byte pitch, CutOffType ignoreCutOff) { // 音高から鍵盤の形状を取得する (KeyType type, CutOffType cutOff) = GetKeyShape(pitch); // 左右の切り欠きを無視する必要があればここで取り除く cutOff &= ~ignoreCutOff; if (type == KeyType.Wide) { Shape element = CreateWideKeyShape(cutOff); Canvas.SetLeft(element, curX); Canvas.SetTop(element, 0); canvas.Children.Add(element); // curX = その時点で描画されている白鍵の右端 curX += WideKeyWidth; } else /* if(type == KeyType.Black) */ { Shape element = CreateNarrowKeyShape(); // (curX = その時点で描画されている白鍵の右端) なので、黒鍵の半分だけ左に食い込ませる Canvas.SetLeft(element, curX - NarrowKeyWidth / 2); Canvas.SetTop(element, 0); canvas.Children.Add(element); } return curX; } private Shape CreateNarrowKeyShape() { // 黒鍵の色と形状 return new Polygon { Fill = Brushes.Black, Stroke = Brushes.CadetBlue, StrokeThickness = 2, Points = new PointCollection { // Left side new Point(0, 0), // Bottom side new Point(0, NarrowKeyHeight), new Point(NarrowKeyWidth, NarrowKeyHeight), // Right side new Point(NarrowKeyWidth, 0) } }; } private Shape CreateWideKeyShape(CutOffType cutOff) { // 白鍵の共通項目 Polygon shape = new Polygon { Fill = Brushes.White, Stroke = Brushes.OrangeRed, StrokeThickness = 2, }; // 切り欠きの位置に応じて形状を決定する switch (cutOff) { case CutOffType.CutLeft: shape.Points = new PointCollection { // Left side new Point(NarrowKeyWidth / 2, 0), new Point(NarrowKeyWidth / 2, NarrowKeyHeight), new Point(0, NarrowKeyHeight), // Bottom side new Point(0, WideKeyHeight), new Point(WideKeyWidth, WideKeyHeight), // Right side new Point(WideKeyWidth, 0) }; break; case CutOffType.CutRight: shape.Points = new PointCollection { // Left side new Point(0, 0), // Bottom side new Point(0, WideKeyHeight), new Point(WideKeyWidth, WideKeyHeight), // Right side new Point(WideKeyWidth, NarrowKeyHeight), new Point(WideKeyWidth - NarrowKeyWidth / 2, NarrowKeyHeight), new Point(WideKeyWidth - NarrowKeyWidth / 2, 0) }; break; case CutOffType.CutBoth: shape.Points = new PointCollection { // Left side new Point(NarrowKeyWidth / 2, 0), new Point(NarrowKeyWidth / 2, NarrowKeyHeight), new Point(0, NarrowKeyHeight), // Bottom side new Point(0, WideKeyHeight), new Point(WideKeyWidth, WideKeyHeight), // Right side new Point(WideKeyWidth, NarrowKeyHeight), new Point(WideKeyWidth - NarrowKeyWidth / 2, NarrowKeyHeight), new Point(WideKeyWidth - NarrowKeyWidth / 2, 0) }; break; case CutOffType.None: shape.Points = new PointCollection { // Left side new Point(0, 0), // Bottom side new Point(0, WideKeyHeight), new Point(WideKeyWidth, WideKeyHeight), // Right side new Point(WideKeyWidth, 0) }; break; default: throw new InvalidOperationException($"Unknown cutOffType: {cutOff}"); } return shape; } private (KeyType type, CutOffType cutOff) GetKeyShape(byte pitch) { // 鍵盤の色と切り欠きの位置を取得する switch (pitch % 12) { case 0:// C case 5:// F return (KeyType.Wide, CutOffType.CutRight); case 4:// E case 11:// B return (KeyType.Wide, CutOffType.CutLeft); case 2:// D case 7:// G case 9:// A return (KeyType.Wide, CutOffType.CutBoth); case 1:// Db case 3:// Eb case 6:// Gb case 8:// Ab case 10:// Bb return (KeyType.Narrow, CutOffType.None); default: throw new ArgumentException($"Invalid pitch: {pitch}", "pitch"); } } 途中経過 ここまでの内容で動かすと以下のようになるはず マウスオーバーで鍵盤の色が変わるようにする 鍵盤の色をプロパティに定義(せっかくなのでデフォルトではグラデーションをかける) PianoKeyboardControl.cs // マウスオーバーしているときの白鍵の色 public static readonly DependencyProperty SelectedWideKeyColorProperty = DependencyProperty.Register(nameof(SelectedWideKeyColor), typeof(Brush), typeof(PianoKeyboardControl), new PropertyMetadata(new LinearGradientBrush(new GradientStopCollection() { new GradientStop(Colors.Red, 0.7), new GradientStop(Colors.LightSalmon, 1.0) }), (sender, args) => { PianoKeyboardControl control = sender as PianoKeyboardControl; if (control.IsLoaded) control.CreateKeyboard(); })); public Brush SelectedWideKeyColor { get { return (Brush) GetValue(SelectedWideKeyColorProperty); } set { SetValue(SelectedWideKeyColorProperty, value); } } // マウスオーバーしているときの黒鍵の色 public static readonly DependencyProperty SelectedNarrowKeyColorProperty = DependencyProperty.Register(nameof(SelectedNarrowKeyColor), typeof(Brush), typeof(PianoKeyboardControl), new PropertyMetadata(new LinearGradientBrush(new GradientStopCollection() { new GradientStop(Colors.LightSteelBlue, 0.0), new GradientStop(Colors.Blue, 0.7) }), (sender, args) => { PianoKeyboardControl control = sender as PianoKeyboardControl; if (control.IsLoaded) control.CreateKeyboard(); })); public Brush SelectedNarrowKeyColor { get { return (Brush) GetValue(SelectedNarrowKeyColorProperty); } set { SetValue(SelectedNarrowKeyColorProperty, value); } } // マウスオーバーしていないときの白鍵の色 public static readonly DependencyProperty NotSelectedWideKeyColorProperty = DependencyProperty.Register(nameof(NotSelectedWideKeyColor), typeof(Brush), typeof(PianoKeyboardControl), new PropertyMetadata(new LinearGradientBrush(new GradientStopCollection() { new GradientStop(Colors.AliceBlue, 0.0), new GradientStop(Colors.White, 0.7) }), (sender, args) => { PianoKeyboardControl control = sender as PianoKeyboardControl; if (control.IsLoaded) control.CreateKeyboard(); })); public Brush NotSelectedWideKeyColor { get { return (Brush) GetValue(NotSelectedWideKeyColorProperty); } set { SetValue(NotSelectedWideKeyColorProperty, value); } } // マウスオーバーしていないときの黒鍵の色 public static readonly DependencyProperty NotSelectedNarrowKeyColorProperty = DependencyProperty.Register(nameof(NotSelectedNarrowKeyColor), typeof(Brush), typeof(PianoKeyboardControl), new PropertyMetadata(new LinearGradientBrush(new GradientStopCollection() { new GradientStop(Colors.Black, 0.7), new GradientStop(Colors.DarkGray, 1.0) }), (sender, args) => { PianoKeyboardControl control = sender as PianoKeyboardControl; if (control.IsLoaded) control.CreateKeyboard(); })); public Brush NotSelectedNarrowKeyColor { get { return (Brush) GetValue(NotSelectedNarrowKeyColorProperty); } set { SetValue(NotSelectedNarrowKeyColorProperty, value); } } PianoKeyboardControl.xaml にスタイルを定義 PianoKeyboardControl.xaml <UserControl.Resources> <Style x:Key="WideKeyStyle" TargetType="Shape"> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Fill" Value="{Binding SelectedWideKeyColor, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:PianoKeyboardControl}}"/> </Trigger> <Trigger Property="IsMouseOver" Value="False"> <Setter Property="Fill" Value="{Binding NotSelectedWideKeyColor, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:PianoKeyboardControl}}"/> </Trigger> </Style.Triggers> </Style> <Style x:Key="NarrowKeyStyle" TargetType="Shape"> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Fill" Value="{Binding SelectedNarrowKeyColor, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:PianoKeyboardControl}}"/> </Trigger> <Trigger Property="IsMouseOver" Value="False"> <Setter Property="Fill" Value="{Binding NotSelectedNarrowKeyColor, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:PianoKeyboardControl}}"/> </Trigger> </Style.Triggers> </Style> </UserControl.Resources> C#側で Shape.Fill を定義する代わりに Shape.Style を定義するよう変更 PianoKeyboardControl.cs private Shape CreateNarrowKeyShape() { // 黒鍵の色と形状 return new Polygon { // ここで Fill を削除し、Style を追加する // Fill = NotSelectedNarrowKeyColor, Style = FindResource("NarrowKeyStyle") as Style, Stroke = Brushes.CadetBlue, StrokeThickness = 2, : : : (中略) }// CreateNarrowKeyShape() private Shape CreateWideKeyShape(CutOffType cutOff) { // 白鍵の共通項目 Polygon shape = new Polygon { // ここで Fill を削除し、Style を追加する // Fill = NotSelectedWideKeyColor, Style = FindResource("WideKeyStyle") as Style, Stroke = Brushes.OrangeRed, StrokeThickness = 2, }; : : : (後略) 途中経過 ここまでの内容で動かすと以下のようになるはず 白鍵にマウスを当てたとき 黒鍵にマウスを当てたとき (ついでに)鍵盤の色を変える 鍵盤の色をプロパティにしたので、ユーザーコントロールの利用側から鍵盤の色を指定できるようになった DemoPage.xaml <!-- ~KeyColor 属性に Brush を設定することで好きな色が使える --> <local:PianoKeyboardControl x:Name="keyboard" NotSelectedWideKeyColor="DarkSlateBlue" SelectedWideKeyColor="SlateBlue" NotSelectedNarrowKeyColor="Bisque" SelectedNarrowKeyColor="LightSalmon"/> クリックしたときに音が鳴るようにする MIDI を扱うためにライブラリを追加する 参照にMicrosoft.Windows.SDK.Contractsする 1. IDE のソリューションエクスプローラーでプロジェクトを右クリック 2. NuGet パッケージの管理(N)... 3. 「参照」タブ 4. 検索窓に「Microsoft.Windows.SDK.Contracts」と入力して検索 5. 「インストール」ボタンを押す 6. (設定次第で)「変更のプレビュー」というウィンドウが開くので「OK」 7. 「ライセンスへの同意」ウィンドウが開くので「同意する」 これでMIDIが扱えるようになった(Windows.Devices.Enumeration と Windows.Devices.Midi 名前空間) PianoKeyboardControl.csを編集する PianoKeyboardControl.cs // [追加] MIDI の出力ポート private IMidiOutPort outPort; public PianoKeyboardControl() { InitializeComponent(); // [追加] MIDIポートを取得する _ = PrepareMidiOutPort(); CreateKeyboard(); } // [追加] private async Task PrepareMidiOutPort() { // PC上に存在する MIDI の出力ポートを検索して取得する string selector = MidiOutPort.GetDeviceSelector(); DeviceInformationCollection deviceInformationCollection = await DeviceInformation.FindAllAsync(selector); if (deviceInformationCollection?.Count > 0) { // 出力ポートがひとつ以上取得できた場合はとりあえず先頭のポートを使用する // Windows の場合は普通 Microsoft GS Wavetable Synth が取得できるはず string id = deviceInformationCollection[0].Id; outPort = await MidiOutPort.FromIdAsync(id); } else { // 出力ポートが取得できない場合はエラー throw new InvalidOperationException($"No MIDI device for {selector}"); } } : : : (中略) private int AddKey(int curX, byte pitch, CutOffType ignoreCutOff) { // 音高から鍵盤の形状を取得する (KeyType type, CutOffType cutOff) = GetKeyShape(pitch); // 左右の切り欠きを無視する必要があればここで取り除く cutOff &= ~ignoreCutOff; if (type == KeyType.Wide) { Shape element = CreateWideKeyShape(cutOff); Canvas.SetLeft(element, curX); Canvas.SetTop(element, 0); // [追加] クリックされたときに音を出し、(左ボタン/マウスカーソルを)離したときに音を止める element.MouseLeftButtonDown += (sender, eventArgs) => NoteOn(pitch); element.MouseLeftButtonUp += (sender, eventArgs) => NoteOff(pitch); element.MouseLeave += (sender, eventArgs) => NoteOff(pitch); canvas.Children.Add(element); // curX = その時点で描画されている白鍵の右端 curX += WideKeyWidth; } else /* if(type == KeyType.Black) */ { Shape element = CreateNarrowKeyShape(); Canvas.SetLeft(element, curX - NarrowKeyWidth / 2); Canvas.SetTop(element, 0); // [追加] クリックされたときに音を出し、(左ボタン/マウスカーソルを)離したときに音を止める element.MouseLeftButtonDown += (sender, eventArgs) => NoteOn(pitch); element.MouseLeftButtonUp += (sender, eventArgs) => NoteOff(pitch); element.MouseLeave += (sender, eventArgs) => NoteOff(pitch); canvas.Children.Add(element); } return curX; } [追加] 指定した高さの音を鳴らす private void NoteOn(byte note) { if(outPort == null) return; IMidiMessage messageToSend = new MidiNoteOnMessage(0, note, 80); outPort.SendMessage(messageToSend); } [追加] 指定した高さの音を止める private void NoteOff(byte note) { if(outPort == null) return; IMidiMessage messageToSend = new MidiNoteOnMessage(0, note, 0); outPort.SendMessage(messageToSend); } : : : (後略) 完成 ここまでの内容で動かすと、鍵盤をクリックすることでピアノの音が鳴る 最後に ここまで読んでいただいてありがとうございます。 C#初心者がQiitaに初めて投稿した記事ということでつたない部分も多かったとは思いますが、誰かの何かに役立てばうれしいです。 以上、長い記事となりましたが改めてありがとうございました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

特定のサイトの更新情報をLINEに通知してくれるスクレイピングアプリを作ってみた!

はじめに 特定のサイトのお知らせ欄が更新されたかな?と毎回見に行くことって結構あるかと思うのですが、時にはやはり忘れてしまうものです。 サイト自体に通知機能のようなものがあればそれを利用するのも一つの対策ですが、そういうものがない場合は毎日確認することをルーチン化する!...いや...やはりそういうわけにはいきません。 そこで今回は特定のサイトのお知らせ欄を1日に2回見にいき、更新されていたらその記事をLINEで通知してくれる、というアプリを作ってみました。 完成形 LINE Notifyから毎日7:00と17:00に以下のように通知が来るようになっています。 記事が更新されていたとき 記事が更新されていないとき アプリ概要と構成図 Amazon EventBridgeが特定の時間になったらLambda関数を実行します。 Lambda関数は実行後、特定のサイトからお知らせ内容をスクレイピングし、LINE Notifyを通して特定のグループLINEへ結果を通知する作りとなっています。 EventBridgeとLambda関数はserverless.templateで管理しています。 アプリ構成図は以下です。 処理手順 簡潔ではありますがアプリの処理手順を以下に記載します。 ①特定サイトのお知らせ欄をスクレイピング 時間になるとAWS Event Bridgeがアプリを起動します。 起動したアプリが特定サイトへ行き、お知らせ欄をガバっと取得(※)します。 ※お知らせ欄は最新の記事10件程度しか掲載されていない作りになっていたので全取得するというロジックにしています。 ②①で取得したお知らせ一覧から通知していないものを絞り込み & 保存 AWS S3にCSVファイル形式で1度通知したお知らせを保管しており、①で取得したお知らせ一覧の中でCSVファイルに保管されていないものを絞り込みます。 絞り込まれたお知らせたちが今回の通知対象となります(今回通知対象のものがある場合はそのデータを新規にCSVファイルに書き込みます)。 ※保管方法についてはDBを利用したりその他色々あるかとは思いますが、DB使うと少し費用かかるかな?というのとこのアプリを作っている時期に業務にてCSV Helperという.NETのライブラリを久々に利用する予定だったのでそれの扱いを思い出しておこうという理由からこのような保管形式としました。 ③②で取得したお知らせをLineで通知 ②で取得したお知らせをLineで通知します。 それぞれの処理で使用したライブラリやサービスなど ①スクレイピング:AngleSharp 特定のサイトのお知らせ欄をスクレイピングする処理にはAngleSharpという.NET向けのライブラリを利用しました。 QuerySelectorやGetElementByIdなどJavaScriptでよく使うメソッドが利用できるので、特定のサイトの欲しい箇所を取得するのも結構楽にできました。 public async ValueTask<IHtmlCollection<IElement>> GetTopicDataAsync() { using var stream = await _clientFactory.CreateClient().GetStreamAsync(new Uri(_scrapingTargetsConfig.Url)); IHtmlDocument doc = await new HtmlParser().ParseDocumentAsync(stream); return doc.GetElementById("TopicId").QuerySelectorAll("table > tbody > tr"); } ②データ保存: AWSSDK.S3, AWSSDK.Extensions.NETCore.Setup, CSV Helper 当初はバッチ実行から1日前までの時間内のトピックのみ取得する、というロジックを組んでいたのですが、スクレイピング先のサイトのバグなのか、投稿日時が2日ほどずれており正常にトピックが取得できませんでした。 そこで、AWS S3へCSVファイルを保存し、そのCSVファイルに取得したものを保存し、そこにないもののみスクレイピングで取得していくというロジックへ変更しました。 CSVの操作については先ほど記載したCSV Helperを、ASP.NET CoreでS3の操作をする処理についてもAWSSDK.S3, AWSSDK.Extensions.NETCore.SetupパッケージをNugetから取得することで比較的楽に作成することができました。 public async ValueTask<GetObjectResponse> GetCsvFile() => await _amazonS3Client.GetObjectAsync(_awsConfig.S3.BacketName, _awsConfig.S3.Key); public async ValueTask UpdateCsvFile() { var putRequest = new PutObjectRequest { BucketName = _awsConfig.S3.BacketName, Key = _awsConfig.S3.Key, FilePath = _localStorageConfig.CsvPath }; await _amazonS3Client.PutObjectAsync(putRequest); } ③LINEでの通知処理: LINE Notify LINEへの送信についてはLINE Notifyというサービスを利用することで作成しました。 以下の記事にまとめましたが、とても良いサービスなので今後も似た処理を作る際は重宝しそうです。 (おまけ)インフラをコード化 こちらは以下の記事にまとめましたが、Visual StudioでServerlessプロジェクトを起動するとserverless.templateにてインフラ構成をJson形式で作成することができます。 EventBridgeなどもコード側で調整してサクッと反映させることができるのでとても良い感じでした。 おわりに 作った当初はLINEではなくメールで送信する感じだったり、AWS上にデプロイせずに自身のローカルPCのタスクスケジューラにセットしていたりと、今とは全然違う構成でした。 「こんな感じのほうが便利かな?」という感じでどんどん途中変更したり、前の現場の上司が(ありがたいことに)見てくださって「ここは〇〇したら面白そう!」みたいにアドバイスをくれたりという感じで実装が変わっていったのですが、それを通してたくさんの技術が学べてとても楽しかったです。 現在はこのLINE通知に任せっきりで、特に自分からサイトを確認しに行くことはなくなりました(笑) ちょっとしたことでも日々の面倒なことを解決するアプリを作ると生活も楽になりますし、技術的にも新しいことが学べるので今後も続けていきたいと思います!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Prefabを一定間隔でランダムな場所に生成

HDRPでも動くこと確認済み。 蝶々の.vfxで試したのでバタフライメイカーという名前になってます もう少し使いやすく改良します、いずれ using System.Collections; using System.Collections.Generic; using UnityEngine; public class ButterFliesMaker : MonoBehaviour { public GameObject butterFliesPrefab; float span = 1.0f; float delta = 0; void Update() { this.delta += Time.deltaTime; if(this.delta > this.span) { this.delta = 0; GameObject birth = Instantiate(butterFliesPrefab); int px = Random.Range(-7, 6); birth.transform.position = new Vector3(px, 5.0f,0.0f); } } }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AIでツイッターユーザーの年代や性別、職業・年収等を判定するサービス「あの人どんな人?」をリリース

はじめに 概要 AIで直近ツイート200件から ツイッターユーザーを解析できるサービスです 年代や性別、職業・年収・既婚/未婚など10項目以上の判定可能です 発端 COTOHA API というのを見つけて、リファレンスを読んでみると 「ユーザ属性推定(β)」という面白そうなものがあったからです twitterにおけるユーザのtweetやprofileの入力を前提とするようにチューニングをしています。 とあるので、ツイートをAPIに投げ込めば良さげなものが作れそうと思いました 技術スタック フロントエンド typescript React Firebase Hosting バックエンド C# Heroku データベース Firebase RealtimeDatabase 開発に関して 特に新しいことをやるわけではなかったので いつも通り既存のリソースを活かしてサクッと作りました 下記2つは新しい試みだったので楽しかったです COTOHAPIのライブラリを作れたこと Reactを使えたこと 副産物 pythonかC#で作ろうと考えていましたが 調べてみるとサンプルコードが少なそうだったのでC#で作ることにしました また、作るにあたってライブラリにしたら、みんな使えてみんな良いと思い、ライブラリにしました どうぞお使いください
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Blazor WebAssemblyを使った簡単なレイトレーシング

はじめに Blazorでレイトレーシングやってたじゃない?ということでBlazorのアドベントカレンダーを紹介していただいたので記事にさせていただきました。 この記事で作成するのは簡単なレイトレーシングにはなりますが、より作りこんでいけば重くなる処理ですのでBlazor WebAssemblyを使用するのが良さそうな分野なのではないかと思います。 レンダリング結果: なぜレイトレーシングを書こうかと思ったのかということですが、ディズニーアニメーションスタジオの作品を観ていたらテンションが上がってきたためでした。 今回は、レイトレーシング自体は一回の交差判定のみですのでレイトレーシングのトレーシング感が薄れてしまうかもしれませんが、スタート地点というように解釈していただければ幸いです。 レイトレーシング(Raytracing)とは? コンピューターによって画像を生成する一つの方法ですが、基本的にはカメラ(目)に入る光をシミュレーションする手法になります。実際のプロダクションで使われるレンダラー(Renderer)は物体に当たった光の反射も含めてレイ(Ray)を追って(Trace)いきますので膨大な計算が必要になります。 この記事では単純にカメラからレイを放出して物体に一回レイが当たるところまでを書いてみます。 処理の流れ レンダリング開始のボタンを押すと処理が開始 カメラから三角形に対してレイを飛ばして交差しているかチェックする 交差している場合は緑色で塗りつぶす。(実際にはそこから再度レイを飛ばしたりします) レイトレーシングの処理が終わったら、結果をCanvasに書き込む レイトレーシングの計算 レンダリングを行うRendererクラスです。Renderメソッドで実際のレイと三角形の交差判定を行っています。 Renderer.cs using System.Threading.Tasks; namespace WebRaytrace { public class Renderer { private Vector3DF[,] pixels; public uint[] UIntPixels { get { // RGBA uint[] uIntPixels = new uint[256 * 256 * (3 + 1)]; int i = 0; for (int y = 0; y < 256; y++) { for (int x = 0; x < 256; x++) { uIntPixels[i + 0] = (uint)(pixels[x, y].X * 255.0f); uIntPixels[i + 1] = (uint)(pixels[x, y].Y * 255.0f); uIntPixels[i + 2] = (uint)(pixels[x, y].Z * 255.0f); uIntPixels[i + 3] = 255; i += (3 + 1); } } return uIntPixels; } } public Renderer() { pixels = null; } public void Render() { pixels = new Vector3DF[256, 256]; Vector3DF camera = new Vector3DF(0.0f, 0.0f, 64.0f); for (int y = -128; y < 128; y++) { for (int x = -128; x < 128; x++) { pixels[x + 128, y + 128] = new Vector3DF(0.0f, 0.0f, 0.0f); Vector3DF screen = new Vector3DF(x, y, 0.0f); Ray ray = new Ray(); ray.Direction = (screen - camera); ray.Position = camera; Vector3DF v1 = new Vector3DF(-50.0f, 0.0f, -30.0f); Vector3DF v2 = new Vector3DF(10.0f, 50.0f, -30.0f); Vector3DF v3 = new Vector3DF(30.0f, -10.0f, -30.0f); Intersection intersection = ray.FindIntersectionWithTriangle(v1, v2, v3); if (intersection.HasIntersection == true) { pixels[x + 128, y + 128] = new Vector3DF(0.0f, 1.0f, 0.0f); } } } } } } レンダリングの処理の実行と結果を画面に表示 index.htmlにJavaScriptでCanvasにレンダリングが完了して受け取ったピクセルの色情報を書き込んでいくメソッドを追加します。 ピクセル情報は1次元の配列で渡していますので、画像のサイズ256x256に合わせて書き込みます。 index.html <!-- bodyのscriptタグの後などに --> <script> window.jsMethod = (pixels) => { var canvas = document.getElementById('canvas'); ctx = canvas.getContext('2d'); var width = 256; var height = 256; var imageData = ctx.createImageData(width, height); var data = imageData.data; for (var i = 0; i < data.length; i += 4) { data[i + 0] = pixels[i + 0]; data[i + 1] = pixels[i + 1]; data[i + 2] = pixels[i + 2]; data[i + 3] = pixels[i + 3]; } ctx.putImageData(imageData, 0, 0); }; </script> レンダリングを行うrazorファイルに記述していきます。 レンダリング用.razor @inject IJSRuntime JS レンダリング開始のボタンとレンダリング先になるCanvasタグを追加します。 レンダリング用.razor <button class="btn btn-primary" @onclick="Render">Render</button> <canvas id="canvas" width="256", height="256"> JavaScriptにピクセル情報(pixels)を渡しています。JSRuntimeのInvokeVoidAsyncを使用するとJavaScriptのメソッドを呼び出して値を渡すことができます。 レンダリング用.razor @code { private async void Render() { Renderer renderer = new Renderer(); renderer.Render(); uint[] pixels = renderer.UIntPixels; await JS.InvokeVoidAsync("jsMethod", pixels); } } おわりに いかがでしたでしょうか? テンションが上がってきた方はぜひ拡張して楽しんでいただけたらと思います! (参考用)ベクトルと交差判定に関するコード レイトレーシングに必要な最小限の(久しぶりに書いた)ベクトルの計算のコードになります。 Vector3DF.cs using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace WebRaytrace { public struct Vector3DF { public float X { get; set; } public float Y { get; set; } public float Z { get; set; } public Vector3DF(float x, float y, float z) { this.X = x; this.Y = y; this.Z = z; } public Vector3DF Normalize { get { float length = Length; return new Vector3DF(X / length, Y / length, Z / length); } } public static Vector3DF operator +(Vector3DF a, Vector3DF b) { return new Vector3DF(a.X + b.X, a.Y + b.Y, a.Z + b.Z); } public static Vector3DF operator -(Vector3DF a, Vector3DF b) { return new Vector3DF(a.X - b.X, a.Y - b.Y, a.Z - b.Z); } public static Vector3DF operator *(Vector3DF a, float b) { return new Vector3DF(a.X * b, a.Y * b, a.Z * b); } public static Vector3DF operator /(Vector3DF a, float b) { return new Vector3DF(a.X / b, a.Y / b, a.Z / b); } public static float operator ^(Vector3DF a, Vector3DF b) { return (a.X * b.X) + (a.Y * b.Y) + (a.Z * b.Z); } // Cross Product public static Vector3DF operator %(Vector3DF a, Vector3DF b) { return new Vector3DF(a.Y * b.Z - a.Z * b.Y, a.Z * b.X + a.X * b.Z, a.X * b.Y - a.Y * b.X); } public float Length { get { return MathF.Sqrt(X * X + Y * Y + Z * Z); } } public float LengthPow2 { get { return X * X + Y * Y + Z * Z; } } } } レイのクラスです。交差判定の結果を格納するIntersetionクラスも一緒に書いております。 Ray.cs using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace WebRaytrace { public struct Intersection { public bool HasIntersection { get; set; } public Vector3DF? Position { get; set; } public Intersection(bool hasIntersection, Vector3DF? position) { HasIntersection = hasIntersection; Position = position; } } public struct Ray { public Vector3DF Position { get; set; } public Vector3DF Direction { get; set; } public Intersection FindIntersectionWithTriangle(Vector3DF v1, Vector3DF v2, Vector3DF v3) { // Plane Vector3DF n = ((v2 - v1) % (v3 - v1)).Normalize; Vector3DF l = this.Direction.Normalize; float ln = l ^ n; if (ln == 0.0f) { return new Intersection(false, null); } float d = ((v1 - Position) ^ n) / ln; Vector3DF p = this.Position + (l * d); // 三角形の中に交点が存在しているかチェックする float triangleArea = ((v2 - v1) % (v3 - v1)).Length; float t1 = ((v1 - p) % (v2 - p)).Length; float t2 = ((v2 - p) % (v3 - p)).Length; float t3 = ((v3 - p) % (v1 - p)).Length; float area = triangleArea - (t1 + t2 + t3); if (Math.Abs(area) < 0.002f) { return new Intersection(true, p); } return new Intersection(false, null); } } } 参考にさせていただいたURL https://en.wikipedia.org/wiki/Line%E2%80%93plane_intersection https://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AD%E3%82%B9%E7%A9%8D
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

地面の種類によって足音を変え,音声が再生される間隔も調整する処理

だいぶ前の話にはなるんですけど,Unity公式がリリースしているLost Cryptというサンプルプロジェクト内でいい感じのコードの書き方を発見し,それを調整したものを現在も使用しているので共有します。 判定したい地面レイヤーを設定 自分の場合はこんな感じでいろいろ。 地面を判定する 大きく分けてコライダーで判定する方法と,Rayで判定する方法の2つがあります。 コライダーのOnCollisionEnter等のイベント関数で判定をすると壁に接触した時にも地面と同じ判定が出る為,今回はRayで地面の検知をします。 // 地面の種類 public enum GroundType { Ground_Grass, // 草 Ground_Stone, // 石 // 一部省略 Airborne // 空中 } private GroundType groundType = GroundType.Airborne; // 取得するレイヤー情報 private int ground_Grass; private int ground_Stone; [SerializeField] private float rayDistance; private void Start() { ground_Grass = LayerMask.GetMask(nameof(GroundType.Ground_Grass)); ground_Stone = LayerMask.GetMask(nameof(GroundType.Ground_Stone)); } private void UpdateGrounding() { if (GroundRayHit(ground_Grass)) { groundType = GroundType.Ground_Grass; } else if (GroundRayHit(ground_Stone)) { groundType = GroundType.Ground_Stone; } else { groundType = GroundType.Airborne; } // 指定したレイヤーが地面にヒットしたかどうか bool GroundRayHit(int layer) { var pos = transform.position; // Unityエディタでのみ起動 #if UNITY_EDITOR // シーンビューにRayを視覚化 // 引数(開始地点, 終了地点, Rayの色) Debug.DrawLine(pos, pos + (Vector2.down * rayDistance), Color.red); #endif // 引数(Rayの開始地点, Rayが向かう方向, Rayの判定距離, 判定するレイヤー) return Physics2D.Raycast(pos, Vector2.down, rayDistance, layer); } } 音声側の処理を設定 まずフィールドに下記のような項目を追加します。 [SerializeField] private AudioSource _audioSource; [Header("Audio Clips")] [SerializeField] private AudioClip[] _stepsGrass, _stepsStone; [Header("Steps Interval")] [SerializeField] private float _stepsInterval; private float _stepsTimer; 次にインスペクターから必要な音声をアタッチします。 メソッドも追加します。 // 接地中の地面から足音を再生 private void AudioSteps(GroundType groundType, float speedNormalized) { // 空中だった場合は処理を返す if (groundType == GroundType.Airborne) return; // 経過時間 * プレイヤーの移動速度をタイマーに加算 _stepsTimer += Time.deltaTime * speedNormalized; // 設定した時間を超えた場合 if (_stepsInterval <= _stepsTimer) { // 現在接地中の地面からランダムな着地音を返す _audioSource.PlayOneShot(SelectedRandomSFX(groundType)); // 音が鳴った後はタイマーをリセット _stepsTimer = 0.0f; } } // 接触している地面に応じて地面の接触音をとる private AudioClip SelectedRandomSFX(GroundType groundType) { // 地面のタイプを判定 // 判定したものを配列に代入 AudioClip[] steps = groundType switch { GroundType.Ground_Grass => _stepsGrass, // 草 GroundType.Ground_Stone => _stepsStone, // 石 _ => null }; // 足音の配列の中からランダムに1つを取得 return steps[Random.Range(0, steps.Length)]; } 自分の場合ジャンプから着地した時にも同一の音声を再生する為メソッドを分けていますが,着地音を別に用意する場合はAudioSteps内に直接SelectedRandomSFXの内容を記述してもいいです。 実際に使用する ここまで書いた全ての内容を統合し,実際に使用してみます。 private float walkSpeed = 1.0f; private float dashSpeed => walkSpeed * 2; private Rigitbody rb; private void Start() { rb = GetComponent<Rigitbody>(); } private void Update() { var speed = walkSpeed; if (Input.GetKeyDown(KeyCode.Shift)) { speed = dashSpeed; } rb.velocity = new Vector2(speed, rb.velocity.y); // 横移動の速さに疾走スピードを割る(疾走中だった場合は1になる) // 今回の場合,歩いている時の速度は走っている時の速度の半分に設定しているので,音の再生速度も半分 float horizontalSpeedNormalized = Mathf.Abs(rb.velocity.x) / dashSpeed; AudioSteps(groundType, horizontalSpeedNormalized); } 以上です。 音声側の処理を記述する場所を変更するなどして使用してください。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む