WPF で 3D オブジェクトを回転させる

前回の WPF で 3D オブジェクトを表示するに引き続いて、今回は 3D オブジェクトを回転させます。
図のようにボタンを配置して、6 方向の回転ができるように実装します。

次のようにコードを追加・変更します。

<Window x:Class="DiceRotationWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml&quot;
xmlns:local="clr-namespace:DiceRotationWpf"
Title="Dice Rotation" Height="600" Width="900">
<Window.Resources>
<Style x:Key="FaceStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="White"/>
<Setter Property="Width" Value="30"/>
<Setter Property="Height" Value="30"/>
<Setter Property="TextAlignment" Value="Center"/>
<Setter Property="FontSize" Value="20"/>
</Style>
<Style x:Key="RepeatButtonStyle" TargetType="{x:Type RepeatButton}">
<Setter Property="Width" Value="50"/>
<Setter Property="Height" Value="50"/>
<Setter Property="FontSize" Value="32"/>
</Style>
</Window.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid x:Name="DicePanel" Background="#FF333333">
<Grid Visibility="Hidden">
<TextBlock x:Name="Face1" Style="{DynamicResource FaceStyle}" Text="1" Background="#FF222222"/>
<TextBlock x:Name="Face2" Style="{DynamicResource FaceStyle}" Text="2" Background="#FFDF2C2C"/>
<TextBlock x:Name="Face3" Style="{DynamicResource FaceStyle}" Text="3" Background="#FFEE9319"/>
<TextBlock x:Name="Face4" Style="{DynamicResource FaceStyle}" Text="4" Background="#FFE3E60A"/>
<TextBlock x:Name="Face5" Style="{DynamicResource FaceStyle}" Text="5" Background="#FF29D214"/>
<TextBlock x:Name="Face6" Style="{DynamicResource FaceStyle}" Text="6" Background="#FF4444BB"/>
</Grid>
<Viewport3D>
<Viewport3D.Camera>
<PerspectiveCamera Position="0,0,10"/>
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight/>
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Transform>
<Transform3DGroup>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D Axis="-0.8,0.3,0.5" Angle="60"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<MatrixTransform3D x:Name="matrixTransform"/>
</Transform3DGroup>
</ModelVisual3D.Transform>
<!-- ModelVisual3D.Content は省略 -->
</ModelVisual3D>
</Viewport3D>
</Grid>
<Grid Background="#FFF8F8F8" Grid.Column="1" Width="300">
<Canvas Height="230" Width="230">
<RepeatButton Content="↑" CommandParameter="-x" Style="{DynamicResource RepeatButtonStyle}" Canvas.Left="90" Canvas.Top="20" Click="Rotate_Click"/>
<RepeatButton Content="↓" CommandParameter="+x" Style="{DynamicResource RepeatButtonStyle}" Canvas.Left="90" Canvas.Top="160" Click="Rotate_Click"/>
<RepeatButton Content="←" CommandParameter="-y" Style="{DynamicResource RepeatButtonStyle}" Canvas.Left="20" Canvas.Top="90" Click="Rotate_Click"/>
<RepeatButton Content="→" CommandParameter="+y" Style="{DynamicResource RepeatButtonStyle}" Canvas.Left="160" Canvas.Top="90" Click="Rotate_Click"/>
<RepeatButton Content="↘" CommandParameter="-z" Style="{DynamicResource RepeatButtonStyle}" Canvas.Left="160" Canvas.Top="20" Click="Rotate_Click"/>
<RepeatButton Content="↙" CommandParameter="+z" Style="{DynamicResource RepeatButtonStyle}" Canvas.Left="20" Canvas.Top="20" Click="Rotate_Click"/>
</Canvas>
</Grid>
</Grid>
</Window>
view raw MainWindow.xaml hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media.Media3D;
namespace DiceRotationWpf
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
const double AngleDelta = 5.0;
static readonly Dictionary<string, Vector3D> Axes = new Dictionary<string, Vector3D>
{
{ "-x", Vector3D.Parse("-1,0,0") },
{ "+x", Vector3D.Parse("1,0,0") },
{ "-y", Vector3D.Parse("0,-1,0") },
{ "+y", Vector3D.Parse("0,1,0") },
{ "-z", Vector3D.Parse("0,0,-1") },
{ "+z", Vector3D.Parse("0,0,1") },
};
void Rotate_Click(object sender, RoutedEventArgs e)
{
var button = (RepeatButton)sender;
var command = (string)button.CommandParameter;
matrixTransform.Rotate(Axes[command], AngleDelta);
}
}
public static class Media3DUtility
{
public static void Rotate(this MatrixTransform3D transform, Vector3D axis, double angle)
{
var matrix = transform.Matrix;
matrix.Rotate(new Quaternion(axis, angle));
transform.Matrix = matrix;
}
}
}

ボタンとして RepeatButton を配置しています。
RepeatButton は、押したままにしておけば断続的に Click イベントが発生します。
また回転の状態を表すために、ModelVisual3D.Transform の中で MatrixTransform3D を使います。

回転には、回転軸と回転角度が必要です。

  • 回転軸は、ベクトルで表されます。
  • 回転角度は、回転軸の方向に右ねじを押し込む場合を正とします。

6 つのボタンはそれぞれ、x, y, z 軸を回転軸とした正方向または負方向の回転を表します。
1 回の Click イベントにつき 5° ずつ回転させています。
なお、カメラは z 軸上の正の位置から原点方向を見下ろし、右側が x 軸の正、上側が y 軸の正を表しています。

Click イベントハンドラーの中で、Matrix3D.Rotate メソッドを呼び出すことでオブジェクトを回転させます。
引数には、回転軸と回転角度を表す Quaternion (四元数) を指定します。
このように、回転軸と回転角度がわかっている場合は比較的簡単に実装ができます。

下図は、最初の状態から x 軸のまわりに -60° 回転させたところです。

全体のソースコードは DiceRotationWpf (GitHub) にあります。
マウスまたはタッチのドラッグ操作でも回転できるようになっています。

Dice Rotation

 

前回: WPF で 3D オブジェクトを表示する
次回: 3D における回転の表現と相互変換

作成したサンプル
DiceRotationWpf (GitHub)

バージョン情報
.NET Framework 4.5

参照
3-D グラフィックスの概要
3-D 変換の概要

WPF で 3D オブジェクトを表示する

WPF の 3D グラフィックスの機能を使って、3D オブジェクトを表示する方法を示します。
XAML でさいころのような立方体を描画することを目指します。

Dice (XAML)

先に XAML を示します。この後に簡単な説明が続きます。

<Window x:Class="DiceXamlWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation&quot;
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml&quot;
xmlns:local="clr-namespace:DiceXamlWpf"
Title="Dice (XAML)" Height="600" Width="600">
<Window.Resources>
<Style x:Key="FaceStyle" TargetType="{x:Type TextBlock}">
<Setter Property="Foreground" Value="White"/>
<Setter Property="Width" Value="30"/>
<Setter Property="Height" Value="30"/>
<Setter Property="TextAlignment" Value="Center"/>
<Setter Property="FontSize" Value="20"/>
</Style>
</Window.Resources>
<Grid Background="#FF333333">
<Grid Visibility="Hidden">
<TextBlock x:Name="Face1" Style="{DynamicResource FaceStyle}" Text="1" Background="#FF222222"/>
<TextBlock x:Name="Face2" Style="{DynamicResource FaceStyle}" Text="2" Background="#FFDF2C2C"/>
<TextBlock x:Name="Face3" Style="{DynamicResource FaceStyle}" Text="3" Background="#FFEE9319"/>
<TextBlock x:Name="Face4" Style="{DynamicResource FaceStyle}" Text="4" Background="#FFE3E60A"/>
<TextBlock x:Name="Face5" Style="{DynamicResource FaceStyle}" Text="5" Background="#FF29D214"/>
<TextBlock x:Name="Face6" Style="{DynamicResource FaceStyle}" Text="6" Background="#FF4444BB"/>
</Grid>
<Viewport3D>
<Viewport3D.Camera>
<PerspectiveCamera Position="0,0,10"/>
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<AmbientLight/>
</ModelVisual3D.Content>
</ModelVisual3D>
<ModelVisual3D>
<ModelVisual3D.Transform>
<Transform3DGroup>
<TranslateTransform3D OffsetZ="0" OffsetX="0" OffsetY="0"/>
<ScaleTransform3D ScaleZ="1" ScaleY="1" ScaleX="1"/>
<RotateTransform3D>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D Axis="0.6,0.3,0.7" Angle="-70"/>
</RotateTransform3D.Rotation>
</RotateTransform3D>
<TranslateTransform3D OffsetZ="0" OffsetX="0" OffsetY="0"/>
<TranslateTransform3D OffsetZ="-2" OffsetX="2" OffsetY="1"/>
</Transform3DGroup>
</ModelVisual3D.Transform>
<ModelVisual3D.Content>
<Model3DGroup>
<GeometryModel3D>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush Visual="{Binding ElementName=Face1}"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="-1,-1,1 1,-1,1 1,1,1 -1,1,1" TriangleIndices="0,1,2 0,2,3" TextureCoordinates="0,1 1,1 1,0 0,0"/>
</GeometryModel3D.Geometry>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush Visual="{Binding ElementName=Face2}"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="-1,-1,-1 1,-1,-1 1,-1,1 -1,-1,1" TriangleIndices="0,1,2 0,2,3" TextureCoordinates="0,1 1,1 1,0 0,0"/>
</GeometryModel3D.Geometry>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush Visual="{Binding ElementName=Face3}"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="1,-1,-1 1,1,-1 1,1,1 1,-1,1" TriangleIndices="0,1,2 0,2,3" TextureCoordinates="0,1 1,1 1,0 0,0"/>
</GeometryModel3D.Geometry>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush Visual="{Binding ElementName=Face4}"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="-1,1,-1 -1,-1,-1 -1,-1,1 -1,1,1" TriangleIndices="0,1,2 0,2,3" TextureCoordinates="0,1 1,1 1,0 0,0"/>
</GeometryModel3D.Geometry>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush Visual="{Binding ElementName=Face5}"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="1,1,-1 -1,1,-1 -1,1,1 1,1,1" TriangleIndices="0,1,2 0,2,3" TextureCoordinates="0,1 1,1 1,0 0,0"/>
</GeometryModel3D.Geometry>
</GeometryModel3D>
<GeometryModel3D>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush Visual="{Binding ElementName=Face6}"/>
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
<GeometryModel3D.Geometry>
<MeshGeometry3D Positions="1,-1,-1 -1,-1,-1 -1,1,-1 1,1,-1" TriangleIndices="0,1,2 0,2,3" TextureCoordinates="0,1 1,1 1,0 0,0"/>
</GeometryModel3D.Geometry>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
</Grid>
</Window>
view raw MainWindow.xaml hosted with ❤ by GitHub

 

まず、Viewport3D を配置します。
3D オブジェクトを描画するには次の 3 種類のものが必要で、これらを Viewport3D に設定します。

カメラ
Viewport3D.Camera プロパティに指定します。PerspectiveCamera を使うと、遠近感が出ます。
既定では LookDirection="0,0,-1" UpDirection="0,1,0" となっており、
つまり z 軸の逆方向を向いており、y 軸の正方向が上になっています。

照明
通常のオブジェクトと同様に、Viewport3D.Children プロパティにモデルとして追加します。
AmbientLight を使うと、影がなく、一様な明るさでオブジェクトが表示されます。

オブジェクト
主に ModelVisual3D として定義し、Viewport3D.Children プロパティに追加します。

以下では、ModelVisual3D の設定について書いていきます。
GeometryModel3D でオブジェクトを定義し、ModelVisual3D.Content プロパティにそれを指定します。
複数のオブジェクトをグループ化するには、Model3DGroup を使います。
GeometryModel3D には、マテリアルとジオメトリを指定します。

マテリアル

GeometryModel3D.Material プロパティで、何を表示するかを指定します。
ここでは、さいころの面を描くために 2D の TextBlock を用意しておき、それをテクスチャとして指定しています。
2D の Brush をテクスチャとして利用するには、DiffuseMaterial を使います。

また、BackMaterial プロパティに、Material プロパティと同じものを指定すれば、裏面には表面を反転したものが表示されます。
つまり、「3」の裏面は「ε」のようになります。

ジオメトリ

GeometryModel3D.Geometry プロパティで、どこに表示するかを指定します。
具体的には、MeshGeometry3D を利用して、次のものを指定します。

点の集合 (Positions プロパティ)
メッシュを構成する三角形に分割した場合に、頂点となる点の集合を指定します。

三角形の集合 (TriangleIndices プロパティ)
Positions プロパティで指定した点に0からインデックスを付けて、メッシュを構成する三角形の頂点を列挙します。
三角形の頂点の順番は、表側から見て反時計回りになるように指定します。

テクスチャの位置 (TextureCoordinates プロパティ)
Positions プロパティで指定した各点が、マテリアルの中のどの点に対応するかを指定します。
マテリアルは 2D なので、左上が (0, 0)、右下が (1, 1) です。

例えば、

<MeshGeometry3D Positions="-1,-1,1 1,-1,1 1,1,1 -1,1,1" TriangleIndices="0,1,2 0,2,3" TextureCoordinates="0,1 1,1 1,0 0,0"/>

とした場合は、P0(-1, -1, 1), P1(1, -1, 1), P2(1, 1, 1), P3(-1, 1, 1) の 4 点があり、
メッシュは P0P1P2 と P0P2P3 の 2 つの三角形で構成され、
P0 が左下、P1 が右下、P2 が右上、P3 が左上となるようにテクスチャを描画するという意味になります。
なお、これらの値を XAML 上で列挙するときの区切り文字としては、カンマまたはスペースの両方が使えます。

アフィン変換 (移動、スケール、回転など) について

3D オブジェクトのアフィン変換は、ModelVisual3D.Transform プロパティで指定します。

コードでの記述について

XAML の代わりに C# などの .NET のコードで記述することもできます。
同一の処理が繰り返される場合には、コードで生成したほうが簡単に表現できることもあります。

 

次回: WPF で 3D オブジェクトを回転させる

作成したサンプル
Wpf3DSample (GitHub): コードで描画した例もあります。

バージョン情報
.NET Framework 4.5

参照
3-D グラフィックスの概要
3-D 変換の概要

Bot Framework で Slack の bot を構成する

Microsoft Bot Framework を利用した bot API を公開して、Slack をクライアントとして使えるように構成してみました。
その構築手順と注意点の簡単なメモを残しておきます。

基本的には、次の 2 つの記事をもとにしています。

(1) 環境設定

(2) bot を Web API として開発

  • テンプレートからプロジェクトを作成する
  • プロジェクトのプロパティで [アセンブリ名] を変更する
  • NuGet で Bot Framework を更新する

(3) Azure App Service に配置

  • Visual Studio から発行する

(4) Bot Framework サイトに登録

  • Web.config の appSettings にアカウント情報を設定するには、Azure Portal の [アプリケーション設定] を利用する
  • Web Chat を構成する
    • HTML に Web Chat を埋め込む
  • Slack bot を構成する
    • Slack の設定を追加するときに表示される手順の通りに進める

 

以下は注意点です。

(1) 開発環境における実行について

ローカルの開発環境においては、
Visual Studio で開始した bot API に Bot Framework Channel Emulator から接続することになりますが、
[デバッグなしで開始 (Ctrl+F5)] では 401 エラーとなります。
[デバッグの開始 (F5)] で実行すれば接続できます。

これは、Controller に BotAuthentication 属性が付けられていることによって、
デバッガーにアタッチされているときは認証不要になるためです。
したがって、開発環境では [デバッグの開始 (F5)] で実行しましょう。

(2) クライアント上でのテキストの表示について

テキストは既定で Markdown 形式です。したがって、記号などを使用するときは注意が必要です。
なお、reply.TextFormat を plain に設定しても、Web Chat では無効のようです。
また、reply.TextFormat を plain に設定しても、Slack ではなぜか * が _ と表示されます。

(3) Activity.Type について

Slack からは、activity.Type が ActivityTypes.Message でない要求が送られてくることがあります。
activity.Type の値により処理を分岐させないと、応答メッセージが重複して表示されてしまいます。

(4) URL の送信について

Slack から URL を送信すると、bot 側で受信するテキストでは、URL が < と > で括られます。

 

素因数分解 bot API のコードを示します。
全体のソースコードは、FactorizationBotApi (GitHub) にあります。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using System.Web.Http;
using Microsoft.Bot.Connector;
namespace FactorizationBotApi
{
[BotAuthentication]
public class MessagesController : ApiController
{
public async Task<HttpResponseMessage> Post([FromBody]Activity activity)
{
if (activity.Type != ActivityTypes.Message)
return Request.CreateResponse(HttpStatusCode.OK);
if (string.IsNullOrWhiteSpace(activity.Text))
return Request.CreateResponse(HttpStatusCode.OK);
var connector = new ConnectorClient(new Uri(activity.ServiceUrl));
var echoMessage = $"You sent \"{activity.Text}\".";
await Reply(connector, activity, echoMessage);
var mainMessage = Factorize(activity.Text);
await Reply(connector, activity, mainMessage);
return Request.CreateResponse(HttpStatusCode.OK);
}
static string Factorize(string text)
{
int i;
if (!int.TryParse(text, out i)) return "Send an integer.";
var factorized = MathHelper.Factorize2(Math.Abs(i));
var factorizedString = string.Join(" · ", factorized.Select(p => $"{p.Key}{(p.Value == 1 ? "" : $"^{p.Value}")}"));
return $"{i} = {(i >= 0 ? "" : "-")}{factorizedString}";
}
static async Task Reply(ConnectorClient connector, Activity activity, string message)
{
var reply = activity.CreateReply(message);
await connector.Conversations.ReplyToActivityAsync(reply);
}
}
}

 

Slack で実行したときのスクリーン ショットです。

Factorization bot

 

表情分析 bot については、Bot Framework で Slack bot 構築メモ (Qiita)に書きました。

作成したサンプル
BotSample (GitHub)

バージョン情報
.NET Framework 4.6
Microsoft.Bot.Builder 3.3.0

参照
人工知能パーツ Microsoft Cognitive Services を使った表情分析アプリを作ろう! (Emotion API × Bot Framework 編)
Azure で Web 公開&お手軽 Web Chat を試す
Microsoft Cognitive Services & Bot Framework 概要
Bot Framework Overview

Bot Framework で Slack bot 構築メモ

カテゴリー: クラウド. タグ: , . 2 Comments »