WPF で地球を回してみるテスト
帰ってきた「○○ で地球を回してみる」シリーズ。
今回は、.NET Framework 3.0 以降に含まれる GUI ライブラリである「WPF(Windows Presentation Foundation)」を試してみたいと思います。
0. 事前準備
以下の開発環境を用意します。
1. Visual C# 2013 を起動する
2. プロジェクトの作成
「ファイル」-「新しいプロジェクト」より「WPF アプリケーション」を選択しプロジェクトを新規作成します。
プロジェクト名は「earth」としておきます。
雛形が作成されました。
3. テクスチャファイルを追加する
ソリューションエクスプローラにて「assets」フォルダを作成し、テクスチャ画像ファイル「earth.jpg」(1024x512)を配置します。
4. ソースを修正する。
雛形として作成されたコードを以下のものに置き換えます。
- MainWindow.xaml
<Window x:Class="earth.MainWindow" x:Name="window" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Hello, WPF World!" Width="450" Height="450" Background="black"> <Window.Resources> <BitmapImage x:Key="earthImage" UriSource="/assets/earth.jpg"/> <ImageBrush x:Key="earthBrush" ImageSource="{ StaticResource earthImage }"/> </Window.Resources> <Viewport3D x:Name="viewport"> <Viewport3D.Camera> <PerspectiveCamera x:Name="perspectiveCamera" Position="0,0,3" LookDirection="0,0,-1" UpDirection="0,1,0"/> </Viewport3D.Camera> <ModelVisual3D> <ModelVisual3D.Content> <Model3DGroup x:Name="model3DGroup"> <AmbientLight Color="#ffffff" /> <DirectionalLight x:Name="directionalLight" Color="#ffffff" Direction="-1,-1,-1" /> </Model3DGroup> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> </Window>
- MainWindow.xaml.cs
using System; using System.Windows; using System.Windows.Controls; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Media.Media3D; namespace earth { public partial class MainWindow : Window { private const int maxLongitudes = 100; // maximum private const int maxLatitudes = 100; // maximum private Point3D[,] position = new Point3D[maxLongitudes + 1, maxLatitudes]; private Point[,] texture = new Point[maxLongitudes + 1, maxLatitudes]; private DiffuseMaterial[] frontMaterial = new DiffuseMaterial[maxLatitudes - 1]; private int longitudes; // actual <= maximum private int latitudes; // actual <= maximum private int emptySlices = 0; // cuts slices out of the apple private int IndexOfFirstGeometryModel3DInModel3DGroup; //= no of lights = 2 private Matrix3D matrix = Matrix3D.Identity; private MatrixTransform3D matrixTransform3D; private Quaternion qX = new Quaternion(new Vector3D(1, 0, 0), 1); // rotations around X-axis private Quaternion qY = new Quaternion(new Vector3D(0, 1, 0), 1); // rotations around Y-axis private Quaternion qZ = new Quaternion(new Vector3D(0, 0, 1), 1); // rotations around Z-axis private System.Windows.Threading.DispatcherTimer timer = new System.Windows.Threading.DispatcherTimer(); public MainWindow() { InitializeComponent(); IndexOfFirstGeometryModel3DInModel3DGroup = model3DGroup.Children.Count; longitudes = 30; latitudes = 30; GenerateImageMaterials(); GenerateSphere(longitudes, latitudes); GenerateAllCylinders(); timer.Interval = TimeSpan.FromMilliseconds(1); timer.Tick += TimerOnTick; timer.Start(); } protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) { try { viewport.Width = viewport.Height = window.ActualWidth; } catch { } } private void GenerateImageMaterials() { ImageBrush imageBrush; double flatThickness = 1.0 / (latitudes - 1); double minus = (double)(longitudes - emptySlices); for (int lat = 0; lat < latitudes - 1; lat++) { imageBrush = new ImageBrush((BitmapImage)Resources["earthImage"]); imageBrush.Viewbox = new Rect(0, lat * flatThickness, minus / longitudes, flatThickness); frontMaterial[lat] = new DiffuseMaterial(imageBrush); } } private void GenerateSphere(int longitudes, int latitudes) { double latitudeArcusIncrement = Math.PI / (latitudes - 1); double longitudeArcusIncrement = 2.0 * Math.PI / longitudes; for (int lat = 0; lat < latitudes; lat++) { double latitudeArcus = lat * latitudeArcusIncrement; double radius = Math.Sin(latitudeArcus); double y = Math.Cos(latitudeArcus) + 0.1; double textureY = (double)lat / (latitudes - 1); for (int lon = 0; lon <= longitudes; lon++) { double longitudeArcus = lon * longitudeArcusIncrement; position[lon, lat].X = radius * Math.Cos(longitudeArcus); position[lon, lat].Y = y; position[lon, lat].Z = -radius * Math.Sin(longitudeArcus); texture[lon, lat].X = (double)lon / longitudes; texture[lon, lat].Y = textureY; } } } private void GenerateAllCylinders() { for (int i = model3DGroup.Children.Count - 1; i >= IndexOfFirstGeometryModel3DInModel3DGroup; i--) { model3DGroup.Children.Remove((GeometryModel3D)model3DGroup.Children[i]); } for (int lat = 0; lat < latitudes - 1; lat++) { GeometryModel3D geometryModel3D = new GeometryModel3D(); geometryModel3D.Geometry = GenerateCylinder(lat); geometryModel3D.Material = frontMaterial[lat]; model3DGroup.Children.Add(geometryModel3D); } } private MeshGeometry3D GenerateCylinder(int lat) { MeshGeometry3D meshGeometry3D = new MeshGeometry3D(); for (int lon = 0; lon <= longitudes - emptySlices; lon++) { Point3D p0 = position[lon, lat]; // on the ceiling Point3D p1 = position[lon, lat + 1]; // on the floor meshGeometry3D.Positions.Add(p0); // on the ceiling meshGeometry3D.Positions.Add(p1); // on the floor meshGeometry3D.Normals.Add((Vector3D)p0); // ceiling normal meshGeometry3D.Normals.Add((Vector3D)p1); // floor normal meshGeometry3D.TextureCoordinates.Add(texture[lon, lat]); // on the ceiling meshGeometry3D.TextureCoordinates.Add(texture[lon, lat + 1]); // on the floor } for (int lon = 1; lon < meshGeometry3D.Positions.Count - 2; lon += 2) { meshGeometry3D.TriangleIndices.Add(lon - 1); // left upper point meshGeometry3D.TriangleIndices.Add(lon); // left lower point meshGeometry3D.TriangleIndices.Add(lon + 1); // right upper point //second triangle = right lower part of the rectangle meshGeometry3D.TriangleIndices.Add(lon + 1); // right upper point meshGeometry3D.TriangleIndices.Add(lon); // left lower point meshGeometry3D.TriangleIndices.Add(lon + 2); // right lower point } return meshGeometry3D; } private void TimerOnTick(Object sender, EventArgs args) { matrix.Rotate(qY); matrixTransform3D = new MatrixTransform3D(matrix); for (int i = IndexOfFirstGeometryModel3DInModel3DGroup; i < model3DGroup.Children.Count; i++) { ((GeometryModel3D)model3DGroup.Children[i]).Transform = matrixTransform3D; } } } }
上記、ソースは、
■ Course 3D_WPF Chapter 4: The complete code of Sphere
http://www.miszalok.de/C_3D_WPF/C4_Sphere/C3D_WPFC4_Code.htm
より、コードを抜き出したものになります。
5. 実行してみる
「F5」 でコンパイル&実行になります。
地球が回れば成功です。
XAML デザイナについて
VS2013 付属の XAML デザイナは 3D 空間に関する情報を視覚的に表現する機能が付いているようです。
例えば、ライトを左方向に照らす設定をすると
以下の様にデザイナに表示されるようです。
意外に便利機能かも知れません。
参考情報
■ Course 3D_WPF Chapter 4: The complete code of Sphere
http://www.miszalok.de/C_3D_WPF/C4_Sphere/C3D_WPFC4_Code.htm