CX's Hatena Blog

はてなブログを使ってみるテスト

WPF で地球を回してみるテスト

帰ってきた「○○ で地球を回してみる」シリーズ。

今回は、.NET Framework 3.0 以降に含まれる GUI ライブラリである「WPF(Windows Presentation Foundation)」を試してみたいと思います。

今回日程 シリーズ構成言語
1日目OpenGL で地球を回してみるテスト C/C++
2日目DirectX で地球を回してみるテスト C/C++
3日目Java 3D で地球を回してみるテスト Java
4日目WebGL で地球を回してみるテスト JavaScript
5日目Three.js で地球を回してみるテスト JavaScript
6日目Babylon.js で地球を回してみるテスト JavaScript
7日目gl.enchant.js で地球を回してみるテストJavaScript
8日目PhiloGL で地球を回してみるテスト JavaScript
9日目CubicVR.js で地球を回してみるテスト JavaScript
最終日Away3D で地球を回してみるテストJavaScript
追加+1Flash 版 Away3D で地球を回してみるテストActionScript
追加+2WPF で地球を回してみるテストC# + XAML
追加+3JavaFX 3D で地球を回してみるテストJava

f:id:cx20:20140211205700p:plain

0. 事前準備

以下の開発環境を用意します。

1. Visual C# 2013 を起動する

f:id:cx20:20131027221856p:plain

2. プロジェクトの作成

「ファイル」-「新しいプロジェクト」より「WPF アプリケーション」を選択しプロジェクトを新規作成します。

f:id:cx20:20140211205701p:plain

プロジェクト名は「earth」としておきます。

f:id:cx20:20140211205702p:plain

雛形が作成されました。

3. テクスチャファイルを追加する

ソリューションエクスプローラにて「assets」フォルダを作成し、テクスチャ画像ファイル「earth.jpg」(1024x512)を配置します。

f:id:cx20:20140211233902p:plain

f:id:cx20:20140120002054p:plain

4. ソースを修正する。

雛形として作成されたコードを以下のものに置き換えます。

<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」 でコンパイル&実行になります。

f:id:cx20:20140211205700p:plain

地球が回れば成功です。

XAML デザイナについて

VS2013 付属の XAML デザイナは 3D 空間に関する情報を視覚的に表現する機能が付いているようです。

例えば、ライトを左方向に照らす設定をすると

f:id:cx20:20140211233904p:plain

以下の様にデザイナに表示されるようです。

f:id:cx20:20140211233903p:plain

意外に便利機能かも知れません。

参考情報

■ Course 3D_WPF Chapter 4: The complete code of Sphere

http://www.miszalok.de/C_3D_WPF/C4_Sphere/C3D_WPFC4_Code.htm