Roslyn の構文解析を使ってデバッガーを自作する

// C# Advent Calendar 2018 の 23 日目の記事です。

デバッガーのようなものを自作してみました。

動機

  • 普段は Visual Studio を使っているが、デバッグ時に手動でステップ実行するのが面倒
    • ループなどでステップ数が多い場合
    • 分岐の様子や変数の状態を軽くチェックしたい場合

解決案

  • ステップの時間間隔だけを指定して、デバッガーを自動で実行させる
    • 変数の一覧が表示される
    • 時間間隔をリアルタイムで調節できる
  • .NET Compiler Platform (Roslyn) の構文解析の機能を使い、各ステップの間にデバッグ用のコードを差し込めば実現できそう

結果

というわけで、WPF でプロトタイプ「Tick-tack Debugger」を作ってみた結果、このようになりました。
例として、ニュートン法で平方根を求めています。 (クリックで拡大)

 

解説

以下は概略の技術解説です。
WPF アプリを作成する前に、まず .NET Framework 上のコンソール アプリで実験してみます。
C# の構文解析を使うには、NuGet で Microsoft.CodeAnalysis.CSharp をインストールします。

デバッグ対象となるソースコードにデバッグ コードを挿入し、それを動的にコンパイルして実行する、という方針です。
コンソール アプリのソースコードを以下に示します (全体のソリューションは SyntaxTreeSample にあります)。

using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using DebuggerLib;
namespace DebuggerConsole
{
class Program
{
const string SourcePath = @"..\..\..\NumericConsole\Program.cs";
const string GeneratedPath = @"Program.g.cs";
static void Main(string[] args)
{
// Generates the code for debugging.
var sourceCode = File.ReadAllText(SourcePath);
var generatedCode = SyntaxHelper.InsertBreakpoints(sourceCode);
File.WriteAllText(GeneratedPath, generatedCode, Encoding.UTF8);
// Compiles and loads the assembly.
var provider = CodeDomProvider.CreateProvider("CSharp");
var compilerOption = new CompilerParameters(new[] { "System.Core.dll", "DebuggerLib.dll" }) { GenerateExecutable = true };
var compilerResult = provider.CompileAssemblyFromFile(compilerOption, GeneratedPath);
if (compilerResult.Errors.HasErrors) return;
// Registers the action for breakpoints.
DebugHelper.InfoNotified += (spanStart, spanLength, variables) =>
{
Console.WriteLine(string.Join(", ", variables.Select(v => $"{v.Name}: {v.Value}")));
Console.WriteLine(sourceCode.Substring(spanStart, spanLength));
Thread.Sleep(1000);
};
// Calls the Main method.
var entryPoint = compilerResult.CompiledAssembly.EntryPoint;
entryPoint.Invoke(null, new object[] { new string[0] });
}
}
}
view raw Program.cs hosted with ❤ by GitHub
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using DebugStatement = System.ValueTuple<Microsoft.CodeAnalysis.CSharp.Syntax.StatementSyntax, string[]>;
namespace DebuggerConsole
{
public static class SyntaxHelper
{
public static string InsertBreakpoints(string sourceCode)
{
var root = ParseText(sourceCode);
var method = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.FirstOrDefault(m => m.Identifier.ValueText == "Main")
?? throw new FormatException("The Main method is not found.");
var statements = DetectStatements(method);
var result = sourceCode;
foreach (var (statement, variables) in statements.Reverse())
{
var (span, debugIndex) = GetSpan(statement);
result = result.Insert(debugIndex, $"DebugHelper.NotifyInfo({span.Start}, {span.Length}{ToParamsArrayText(variables)});\r\n");
}
return result.Insert(root.Usings.FullSpan.End, "using DebuggerLib;\r\n");
}
public static CompilationUnitSyntax ParseText(string text)
{
var tree = CSharpSyntaxTree.ParseText(text);
var diagnostics = tree.GetDiagnostics().ToArray();
if (diagnostics.Length > 0) throw new FormatException(diagnostics[0].ToString());
return tree.GetCompilationUnitRoot();
}
public static DebugStatement[] DetectStatements(MethodDeclarationSyntax method)
{
var statements = new List<DebugStatement>();
DetectStatements(method.Body, statements, new List<(string, SyntaxNode)>());
return statements.ToArray();
}
static void DetectStatements(SyntaxNode node, List<DebugStatement> statements, List<(string name, SyntaxNode scope)> variables)
{
// Adds variables.
if (node is VariableDeclarationSyntax varSyntax)
{
var varNames = varSyntax.Variables.Select(v => v.Identifier.ValueText).ToArray();
var scope = ((node.Parent is LocalDeclarationStatementSyntax) ? node.Parent : node)
.Ancestors()
.First(n => n is StatementSyntax);
variables.AddRange(varNames.Select(v => (v, scope)));
}
// Maps variables to the statement.
if ((node is StatementSyntax statement) &&
!(node is BlockSyntax) &&
!(node is BreakStatementSyntax))
statements.Add((statement, variables.Select(v => v.name).ToArray()));
// Recursively.
foreach (var child in node.ChildNodes())
DetectStatements(child, statements, variables);
// Maps variables to the last line of the block.
if (node is BlockSyntax block)
statements.Add((block, variables.Select(v => v.name).ToArray()));
// Clears variables out of the scope.
if (node is StatementSyntax)
for (var i = variables.Count - 1; i >= 0; i--)
if (variables[i].scope == node)
variables.RemoveAt(i);
else
break;
}
static (TextSpan, int) GetSpan(StatementSyntax statement)
{
switch (statement)
{
case ForStatementSyntax f:
var span = new TextSpan(f.ForKeyword.Span.Start, f.CloseParenToken.Span.End - f.ForKeyword.Span.Start);
return (span, statement.FullSpan.Start);
case BlockSyntax b:
return (b.CloseBraceToken.Span, b.CloseBraceToken.FullSpan.Start);
default:
return (statement.Span, statement.FullSpan.Start);
}
}
static string ToParamsArrayText(string[] variables) =>
string.Concat(variables.Select(v => $", new Var(\"{v}\", {v})"));
}
}
view raw SyntaxHelper.cs hosted with ❤ by GitHub

SyntaxHelper クラスでは、デバッグ対象の C# ソースコードを構文ツリー (SyntaxTree) に変換して走査し、
各ステートメントの前にデバッグ用のコード行を挿入していきます。

CSharpSyntaxTree.ParseText メソッドを使うことで、ソースコードを構文ツリーに変換できます。
また、メソッド・ステートメント・式など、すべてのノードを表す親クラスは SyntaxNode クラスであり、

  • Parent プロパティ: 親
  • Ancestors メソッド: 祖先
  • ChildNodes メソッド: 子
  • DescendantNodes メソッド: 子孫

が存在することを知っておけば、だいたいの探索ができるでしょう。

この他に、デバッグ用のコードから呼び出されるメソッドを定義するクラス ライブラリとして DebuggerLib を作成しています。
各ステートメントの位置、およびその直前で存在する変数とその値を通知するために、このライブラリを経由させます。

Program クラスでは、生成されたデバッグ用のソースコードをファイルに保存したら、
System.CodeDom.Compiler 名前空間の CodeDomProvider を使ってこれをコンパイルし、
そのエントリ ポイント (Main メソッド) を呼び出します。
また、デバッグ コードが実行されたときのイベントハンドラーを登録しておき、
Thread.Sleep メソッドを使って、指定した時間だけ停止させます。

これで、デバッグ対象の元のソースコードが次の Program.cs だとすると、
デバッグ用のソースコードとして下の Program.g.cs が生成されます。

using System;
namespace NumericConsole
{
class Program
{
static void Main(string[] args)
{
// Square root by the Newton's method.
var a = 5.0;
var x = a;
for (var i = 0; i < 100; i++)
{
var xi = (x + a / x) / 2;
if (x == xi) break;
x = xi;
}
Console.WriteLine(x);
}
}
}
view raw Program.cs hosted with ❤ by GitHub
using System;
using DebuggerLib;
namespace NumericConsole
{
class Program
{
static void Main(string[] args)
{
DebugHelper.NotifyInfo(188, 12);
// Square root by the Newton's method.
var a = 5.0;
DebugHelper.NotifyInfo(214, 10, new Var("a", a));
var x = a;
DebugHelper.NotifyInfo(240, 29, new Var("a", a), new Var("x", x));
for (var i = 0; i < 100; i++)
{
DebugHelper.NotifyInfo(302, 25, new Var("a", a), new Var("x", x), new Var("i", i));
var xi = (x + a / x) / 2;
DebugHelper.NotifyInfo(347, 19, new Var("a", a), new Var("x", x), new Var("i", i), new Var("xi", xi));
if (x == xi) break;
DebugHelper.NotifyInfo(384, 7, new Var("a", a), new Var("x", x), new Var("i", i), new Var("xi", xi));
x = xi;
DebugHelper.NotifyInfo(405, 1, new Var("a", a), new Var("x", x), new Var("i", i), new Var("xi", xi));
}
DebugHelper.NotifyInfo(422, 21, new Var("a", a), new Var("x", x));
Console.WriteLine(x);
DebugHelper.NotifyInfo(453, 1, new Var("a", a), new Var("x", x));
}
}
}
view raw Program.g.cs hosted with ❤ by GitHub

作成したコンソール アプリを実行すると、次の図のようになります (時間間隔は 0.3 秒)。

 

以上をもとに、WPF アプリでデバッグ ツールを作成しました。
左側の C# ソースコードの部分は TextBox で、編集もできます。
デバッグ実行時は、各ステートメントを選択状態にすることでハイライトしています。
右側の変数一覧が表示される部分は DataGrid です。

(図は円周率を求める例)

今回は上記の方法でプロトタイプを作ってみましたが、
デバッグ コードの挿入やコンパイルに関しては、よりスマートな方法があるのではないかと思います。

注意点
  • 考えられうるすべてのステートメントには対応できていません。また、Main メソッドしか構文解析していません。
  • コンパイル時に生成されるアセンブリ (EXE) は、%TEMP% フォルダー (ユーザーの AppData\Local\Temp) に保存されていきます。
  • TextBox で、IsInactiveSelectionHighlightEnabled を True に設定しても利かないことがあります。
    また、選択状態のハイライトがずれることがあります。
    RichTextBox で Run などを使うのがよいかもしれません。

 

作成したサンプル
バージョン情報
参照

SIR 感染症モデルのシミュレーター

// この投稿は ライフゲーム Advent Calendar 2017 の 19 日目の記事です。

2017年11月11日の稲葉寿先生還暦記念祝賀研究集会「数理人口学・数理疫学・構造化個体群モデル」で講演してきました。
そのときに発表したシミュレーション ツールを紹介します。

数理生物学で「SIR 感染症モデル」という数理モデルがあり、

S(t): 未感染者の人口
I(t): 感染者の人口
R(t): 回復者の人口

としたとき、

S’ = θR – βSI
I’ = βSI – γI
R’ = γI – θR

のような微分方程式系で表されるものを指します。ここで、各定数は

β: 感染率
γ: 回復率
θ: 免疫喪失率

を表します (このように免疫喪失を考慮する場合は SIRS ともいう)。

このモデルをもとに、感染症の伝播を視覚的に表現するセルオートマトンを WPF で作成しました。
各ファイルは GitHub にあります。

EpidemicSimulator.exe を実行し、右下のトグルスイッチを押せばシミュレーションを開始できます。
感染率などのいくつかのパラメーターは実行時にリアルタイムに変更できます。

S と I が隣り合っているとき、一定の割合で感染が発生するようになっています (したがって、数式で表した状況とは厳密には異なります)。
また、I と R は一定の割合でそれぞれ R と S に移動します。

実行前に設定するパラメーター:

  • 高さ
  • SIR の初期人口比

実行中も設定できるパラメーター:

  • 感染率
  • 回復率
  • 免疫喪失率
  • Looping Map: マップの端でループするかどうか
  • ターンの時間間隔

Epidemic Simulator

 

実装方法については技術的に目新しいところはありませんが、特徴としては以下が挙げられます。

(1) シミュレーションの演算は UI スレッドではなく、バックグラウンド スレッドで実行
(2) 各フレームで画像データを生成して、Image コントロールで表示

(1) については、重い処理を UI スレッドで実行するとアプリケーションがフリーズしてしまうため、
各フレームで非同期的にデータのスナップショットを取得しています。
技術的な説明は、以前に

で書いた通りです。

バージョン情報
.NET Framework 4.5
ReactiveProperty 3.6.0
ToggleSwitch 1.1.2

参照
稲葉寿先生還暦記念祝賀研究集会「数理人口学・数理疫学・構造化個体群モデル」

カテゴリー: ツール, 数学. タグ: . Leave a Comment »

Leap Motion で手の回転状態を取得する

Leap Motion Controller の公式 SDK では、手の回転の状態をオイラー角で取得できるようになっています。
具体的には、Hand.Direction (Vector オブジェクト) の Yaw, Pitch, Roll プロパティが用意されています。
ただし、Hand クラスの説明を参照すると、 ロールについては Direction.Roll ではなく PalmNormal.Roll を使うように書かれています。

float pitch = hand.Direction.Pitch;
float yaw = hand.Direction.Yaw;
float roll = hand.PalmNormal.Roll;

しかし、これらの値を使って実装してみても、期待通りの動作にはなりません。

そこで、前回の 3D における回転の表現と相互変換の内容をもとに、手の回転の状態を取得する機能を自作しました。

Hand.Direction と Hand.PalmNormal はともに長さ 1 で直交しているため、
これらをそれぞれ (0, 0, -1) と (0, -1, 0) の回転後のベクトルと見なして、
前回作成した Rotation3DHelper クラスを利用してオイラー角を求めれば OK です。

using System;
using System.Windows.Media.Media3D;
namespace HandRotationLeap
{
public static class Leap3DHelper
{
public static Vector3D ToVector3D(this Leap.Vector v) => new Vector3D(v.x, v.y, v.z);
// Wrong values.
public static EulerAngles GetEulerAngles_org(this Leap.Hand h) => new EulerAngles
{
Yaw = h.Direction.Yaw,
Pitch = h.Direction.Pitch,
Roll = h.PalmNormal.Roll,
};
// Improved values.
public static EulerAngles GetEulerAngles(this Leap.Hand h) =>
Rotation3DHelper.ToEulerAngles(-h.Direction.ToVector3D(), -h.PalmNormal.ToVector3D());
}
}
view raw AppModel.cs hosted with ❤ by GitHub

全体のソースコードは HandRotationLeap (GitHub) にあります。
このサンプルでは、手とさいころの回転の状態を同期させています。

Hand Rotation by Leap Motion Controller

前回: 3D における回転の表現と相互変換

バージョン情報
.NET Framework 4.5
Leap Motion SDK 2.3.1

参照
Hand クラス

3D における回転の表現と相互変換

以前に投稿した WPF で 3D オブジェクトを回転させるではオブジェクトの回転の状態を行列で表していましたが、
3 次元空間における回転を表現する方法は、次のように何通りか考えられます。
なお、原点を中心に回転させるものとし、回転角度は、回転軸を表すベクトルの方向に右ねじを押し込む場合を正とします。

  1. 行列
    • 3 次正方行列。とくに、回転を表すものは直交行列であり、P^{-1} = {}^t P が成り立つ
    • WPF では Matrix3D 構造体
  2. 四元数 (クォータニオン)
  3. オイラー角 (ロール、ピッチ、ヨー)
    • 任意の回転を 3 回の単純な回転の合成により表す。詳細の定義はロール・ピッチ・ヨー ― Kinectで学ぶ数学を参照
    • 座標系ごと回転させる
    • ロール、ピッチ、ヨーをそれぞれどの軸に対応させるか、どの順番で作用させるかで結果が変わってしまうため注意が必要
  4. 特定の 2 点の回転前後の座標
    • とくに、直交する 2 つのベクトルを用いるとよい

3D のプログラミングをしていると、API によってどの表現を利用するかが異なることがあります。
以下では、これらの表現を互いに変換する方法について考えます。

■ 行列 → 2 点の座標

任意のベクトルに対して行列を作用させれば回転後のベクトルが求められます。
WPF では演算子 * が用意されています。

■ 四元数 → 行列

四元数の成分から行列の各成分を計算できます (詳細は四元数による回転行列の表現を参照)。
WPF では Matrix3D.Rotate メソッドとして用意されています。

■ オイラー角 → 行列または四元数

ここでは、ヨーは y 軸、ピッチは x 軸、ロールは z 軸のまわりの回転を表し、この順に適用されるとします。
(元の座標系における) ヨー、ピッチ、ロールを表す回転行列をそれぞれ R_y, R_p, R_r とし、それぞれの回転角度を \theta_y, \theta_p, \theta_r とします。
すなわち、

R_y = \left( \begin{array}{ccc} \cos \theta_y & 0 & \sin \theta_y \\ 0 & 1 & 0 \\ -\sin \theta_y & 0 & \cos \theta_y \end{array} \right), R_p = \left( \begin{array}{ccc} 1 & 0 & 0 \\ 0 & \cos \theta_p & -\sin \theta_p \\ 0 & \sin \theta_p & \cos \theta_p \end{array} \right), R_r = \left( \begin{array}{ccc} \cos \theta_r & -\sin \theta_r & 0 \\ \sin \theta_r & \cos \theta_r & 0 \\ 0 & 0 & 1 \end{array} \right)

このとき、座標系ごとヨー、ピッチ、ロールの順に適用した回転は、元の座標系で R_y R_p R_r で表されます (適用の順序が逆になる)。
証明は次のようにできますが、実際の回転をイメージするとわかりやすいと思います。

証明

ベクトル {\bf x} にヨーを作用させると、
R_y {\bf x}
次に、これにピッチを作用させるには、いったん座標系を戻して R_p を作用させるから、
R_y R_p R_y^{-1} \cdot R_y {\bf x} = R_y R_p {\bf x}
同様に、これにロールを作用させると、
(R_y R_p) R_r (R_y R_p)^{-1} \cdot R_y R_p {\bf x} = R_y R_p R_r {\bf x}
(証明終)

WPF での実際の演算では四元数を使うとよいでしょう。

■ 2 点の座標 → オイラー角

ここでは、2 点として {\bf e}_z = (0, 0, 1), {\bf e}_y = (0, 1, 0) を選びます。
これらの回転後のベクトルがそれぞれ {\bf u}, {\bf t} で与えられたとすると、オイラー角は以下の手続きにより求められます。

まず、{\bf u} の x 要素と z 要素の比はピッチおよびロールの影響を受けないから、
\tan \theta_y = \dfrac{{\bf u}_x}{{\bf u}_z}
により、\theta_y, R_y が決まる。

ピッチおよびロールの決め方から、
\tan \theta_p = \dfrac{- (R_p {\bf e}_z)_y}{(R_p {\bf e}_z)_z}, \tan \theta_r = \dfrac{- (R_r {\bf e}_y)_x}{(R_r {\bf e}_y)_y}

また、前項と同様に考えて、{\bf u} = R_y R_p {\bf e}_z, {\bf t} = R_y R_p R_r {\bf e}_y であるから、
R_p {\bf e}_z = R_y^{-1} {\bf u}, R_r {\bf e}_y = R_p^{-1} R_y^{-1} {\bf t}

したがって、\theta_p, \theta_r も順に決まる。
なお、この決め方の場合、回転角度の範囲は、
- \pi < \theta_y \le \pi, - \dfrac{\pi}{2} \le \theta_p \le \dfrac{\pi}{2}, - \pi < \theta_r \le \pi
となる。
(終)

arctan を求めるには、Math.Atan2 メソッドを使うとよいでしょう。

以上により、行列 → 2 点の座標 → オイラー角 → 四元数 → 行列と変換する方法が与えられたので、
回転の表現を相互に変換できるようになります。

では、これらを実装してみます。
ベクトル、行列、四元数を扱うための System.Numerics.Vectors という、WPF のライブラリよりも高機能なライブラリもあるのですが、
今回は WPF のライブラリのみを利用して実装したいと思います。

using System;
using System.Diagnostics;
using System.Windows.Media.Media3D;
namespace RotationTest
{
public static class Rotation3DHelper
{
public static readonly Vector3D UnitX = new Vector3D(1, 0, 0);
public static readonly Vector3D UnitY = new Vector3D(0, 1, 0);
public static readonly Vector3D UnitZ = new Vector3D(0, 0, 1);
public static double ToDegrees(double radians) => radians * 180 / Math.PI;
public static double ToRadians(double degrees) => degrees * Math.PI / 180;
public static Quaternion CreateQuaternionInRadians(Vector3D axis, double angleInRadians) => new Quaternion(axis, ToDegrees(angleInRadians));
// 四元数 → 行列
public static Matrix3D ToMatrix3D(this Quaternion q)
{
var m = new Matrix3D();
m.Rotate(q);
return m;
}
// オイラー角 → 四元数
public static Quaternion ToQuaternion(this EulerAngles e) =>
CreateQuaternionInRadians(UnitY, e.Yaw) *
CreateQuaternionInRadians(UnitX, e.Pitch) *
CreateQuaternionInRadians(UnitZ, e.Roll);
// 2 点の座標 → オイラー角
public static EulerAngles ToEulerAngles(Vector3D rotatedUnitZ, Vector3D rotatedUnitY)
{
var y_yaw = Math.Atan2(rotatedUnitZ.X, rotatedUnitZ.Z);
var m_yaw_inv = CreateQuaternionInRadians(UnitY, -y_yaw).ToMatrix3D();
rotatedUnitZ = rotatedUnitZ * m_yaw_inv;
rotatedUnitY = rotatedUnitY * m_yaw_inv;
var x_pitch = Math.Atan2(-rotatedUnitZ.Y, rotatedUnitZ.Z);
var m_pitch_inv = CreateQuaternionInRadians(UnitX, -x_pitch).ToMatrix3D();
rotatedUnitY = rotatedUnitY * m_pitch_inv;
// 本来は -rotatedUnitY.X だけでよいはずです。しかし、X=0 のときに π でなく -π となってしまうため、場合分けします。
var z_roll = Math.Atan2(rotatedUnitY.X == 0 ? 0 : -rotatedUnitY.X, rotatedUnitY.Y);
return new EulerAngles { Yaw = y_yaw, Pitch = x_pitch, Roll = z_roll };
}
}
[DebuggerDisplay(@"\{Yaw={Yaw}, Pitch={Pitch}, Roll={Roll}\}")]
public struct EulerAngles
{
public double Yaw { get; set; }
public double Pitch { get; set; }
public double Roll { get; set; }
}
}
view raw Rotation3DHelper.cs hosted with ❤ by GitHub

全体のソースコードは RotationTest (GitHub) にあります。
これらのメソッドを呼び出すテストが付いています。

前回: WPF で 3D オブジェクトを回転させる
次回: Leap Motion で手の回転状態を取得する

バージョン情報
.NET Framework 4.5

参照

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

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

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

<Window x:Class="DiceRotationWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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;
}
}
}
view raw MainWindow.xaml.cs hosted with ❤ by GitHub

ボタンとして 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 変換の概要