任意3D文字が回転するスクリーンセーバー

WPFでTextから2DのPathに変換しました。じゃあ3Dは?というのが浮かんだ内容ですが、これが結構難しい。行きついた情報はこちら、あのProgramming Windowsで有名なCharles Petzold氏のMSDNマガジンへの寄稿でした。

詳細は追って理解するとしてこれを使ったスクリーンセーバを作ってみます。WPFによるスクリーンセーバーの作り方はこちら、非常に分かりやすく解説されています。

 

App.xaml.cs

namespace WpfText3DScreenSaver
{
    using System.Globalization;
    using System.Windows;

    public partial class App : Application
    {
        private MainWindow window = new MainWindow();

        private void Application_Startup(object sender, StartupEventArgs e)
        {
            if (e.Args.Length > 0){
                string mode = e.Args[0].ToLower(CultureInfo.InvariantCulture);
                if (mode.StartsWith("/c")){
                    ShowConfig();
                    return;
                }
                else if (mode.StartsWith("/p"))
                {
                    return;
                }
            }
            SaveScreen();
        }

        private void ShowConfig()
        {
            var settingWindow = new TextSettingWindow();
            settingWindow.ShowDialog();
        }
        private void SaveScreen()
        {
            window.Topmost = true;
            window.Left = 0;
            window.Top = 0;
            window.WindowState = WindowState.Maximized;
            window.Show();
        }
    }
}

App.xaml

<Application x:Class="WpfText3DScreenSaver.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             Startup="Application_Startup">
    <Application.Resources>
    </Application.Resources>
</Application>

StartupUriでなく、Startupになっているのに注意。Startupとしてはcsで定義したハンドラを指定しています。設定ウィンドウはこんな感じ。

 TextSettingWindow.xaml.cs

namespace WpfText3DScreenSaver
{
    using System.Windows;

    public partial class TextSettingWindow : Window
    {
        public TextSettingWindow()
        {
            InitializeComponent();
            textbox.Text = Properties.Settings.Default.Text3D;
            setbutton.Click += (_, __) =>{
                DialogResult = true;
                Properties.Settings.Default.Text3D = textbox.Text;
                Properties.Settings.Default.Save();
                Close();
            };
            cancelbutton.Click += (_, __) =>
            {
                DialogResult = false;
                Close();
            };
        }
    }
}

TextSettingWindow.xaml

<Window x:Class="WpfText3DScreenSaver.TextSettingWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="TextSettingWindow" Height="104" Width="300">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="140"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="2*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBox Name="textbox" Grid.ColumnSpan="3" TextWrapping="Wrap" Text="TextBox"/>
        <Button Name="setbutton"  Content="設定" Grid.Column="1" Grid.Row="1"/>
        <Button Name="cancelbutton" Content="キャンセル" Grid.Column="2" Grid.Row="1"/>
    </Grid>
</Window>

settingwindow

ポイントって言うほどではないんですが、設定時と実行時ではプログラム引数違いなだけでそれぞれ終了してしまうため、設定を保持しておく必要があります。ここではフツーにSettingsを使って設定を保持しておくことにします。

 

さて、メインとなるウィンドウはこちら。

 MainWindow.xaml

<Window x:Class="WpfText3DScreenSaver.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:petzold="clr-namespace:Petzold.Text3D;assembly=Petzold.Text3D"
        Title="MainWindow" Height="350" Width="525" WindowStyle="None">
    <Grid>
        <Viewport3D>
            <petzold:SolidText x:Name="field"
                       Text="任意文字"
                       FontFamily="Times New Roman"
                       FontWeight="Bold"
                       Origin="-1.5 0.5" Depth="0.5">
                <petzold:SolidText.Material>
                    <MaterialGroup>
                        <DiffuseMaterial Brush="Blue" />
                        <SpecularMaterial Brush="White" />
                    </MaterialGroup>
                </petzold:SolidText.Material>

                <petzold:SolidText.BackMaterial>
                    <MaterialGroup>
                        <DiffuseMaterial Brush="Red" />
                        <SpecularMaterial Brush="White" />
                    </MaterialGroup>
                </petzold:SolidText.BackMaterial>

                <petzold:SolidText.SideMaterial>
                    <MaterialGroup>
                        <DiffuseMaterial>
                            <DiffuseMaterial.Brush>
                                <LinearGradientBrush StartPoint="0 0"
                                                 EndPoint="0 1">
                                    <GradientStop Offset="0" Color="Blue" />
                                    <GradientStop Offset="1" Color="Red" />
                                </LinearGradientBrush>
                            </DiffuseMaterial.Brush>
                        </DiffuseMaterial>
                        <SpecularMaterial Brush="White" />
                    </MaterialGroup>
                </petzold:SolidText.SideMaterial>

                 <!--Transform.-->
                <petzold:SolidText.Transform>
                    <RotateTransform3D>
                        <RotateTransform3D.Rotation>
                            <AxisAngleRotation3D x:Name="rotate" Axis="1 1 0" />
                        </RotateTransform3D.Rotation>
                    </RotateTransform3D>
                </petzold:SolidText.Transform>
            </petzold:SolidText>

             <!--Lights.-->
            <ModelVisual3D>
                <ModelVisual3D.Content>
                    <Model3DGroup>
                        <AmbientLight Color="#404040" />
                        <DirectionalLight Color="#C0C0C0" Direction="2 -3 -1" />
                    </Model3DGroup>
                </ModelVisual3D.Content>
            </ModelVisual3D>

             <!--Camera.-->
            <Viewport3D.Camera>
                <PerspectiveCamera Position="0 0 8" UpDirection="0 1 0"
                               LookDirection="0 0 -1" FieldOfView="45" />
            </Viewport3D.Camera>
        </Viewport3D>

         <!--Animation.-->
        <Grid.Triggers>
            <EventTrigger RoutedEvent="Page.Loaded">
                <BeginStoryboard>
                    <Storyboard>
                        <DoubleAnimation Storyboard.TargetName="rotate"
                                     Storyboard.TargetProperty="Angle"
                                     From="0" To="360" Duration="0:0:15"
                                     RepeatBehavior="Forever" />
                    </Storyboard>
                </BeginStoryboard>
            </EventTrigger>
        </Grid.Triggers>
    </Grid>
</Window>

MainWindow.xaml.cs

namespace WpfText3DScreenSaver
{
    using System.Windows;

    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            field.Text = Properties.Settings.Default.Text3D;
            this.MouseMove += (_, __) => Close();
        }
    }
}

3D表示は全体的にPetzold.Text3D DLLに依存しています。MSDNからソースコードも落とせるので落として見ると良いかと。これをexeとしてビルドした後、srcに拡張しを変更して、ファイルを右クリック→インストールを実行した様子はこちら。

setting

これを実行した画面キャプチャした様子はこちら。

scrsave

Text→3Dへの変換は追って理解していくことにします。

WPF3Dことはじめ

WPFの3Dクラスがややこしい。

wpf3d

左から。WPFのWindowやGridに3D表示されるUIElementはViewPort3D。このViewPort3DにはCameraプロパティがあって3Dモデルを映している形にります。その3DモデルはModelVisual3Dオブジェクトですが、通常は複数のオブジェクトを保有するのでModel3DGroupでグループ化してやる必要があります。

Model3DGroupの中にはLight(AmbientLightやDirectionalLight)と目的の3Dモデル(Geometry3Dオブジェクト)を保有する形になる。3Dモデルはまず3頂点で三角形をつくる、これがMeshGeometry3D、それにMaterial(テクスチャ)を貼り付けてGeometry3Dを作る。

 

1.3DオブジェクトをXAMLで作る

3DオブジェクトをXAMLコードでカキカキするのはほぼ不可能、モデラーを使います。Blenderがメジャーぽいけど使いにくかったのでメタセコイアで。初心者向けの書籍もAmazon中古で格安になっていたりするのでお勧め。

metasequoia

モデルを作成後ファイル保存(.mqo)し、Identityで開きXAMLで保存します。アニメーションも作りたい場合はIdentityで作ります。

Identity_

できたXAMLを張り付ければ3Dオブジェクトは完成。

Test.xaml

<Viewbox x:Name="Viewbox_test" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/interactivedesigner/2006" xmlns:c="http://schemas.openxmlformats.org/markup-compatibility/2006" c:Ignorable="d">
	<Viewport3D x:Name="Viewport3D_test" Width="400" Height="300">
		<Viewport3D.Resources>
			<ResourceDictionary>
				<MaterialGroup x:Key="Material_test_NoMaterial" >
					<DiffuseMaterial>
						<DiffuseMaterial.Brush>
							<SolidColorBrush Color="#cccccc" Opacity="1.000000"/>
						</DiffuseMaterial.Brush>
					</DiffuseMaterial>
				</MaterialGroup>
				<Transform3DGroup x:Key="Transform_Character_test_Character_0" >
					<TranslateTransform3D OffsetX="0" OffsetY="0" OffsetZ="0"/>
					<ScaleTransform3D ScaleX="1" ScaleY="1" ScaleZ="1"/>
					<RotateTransform3D CenterX="0" CenterY="0" CenterZ="0">
						<RotateTransform3D.Rotation>
							<AxisAngleRotation3D Angle="0" Axis="0 1 0"/>
						</RotateTransform3D.Rotation>
					</RotateTransform3D>
					<TranslateTransform3D OffsetX="0" OffsetY="0" OffsetZ="0"/>
				</Transform3DGroup>
				<Transform3DGroup x:Key="Transform_Model_test_obj1" >
					<TranslateTransform3D OffsetX="0" OffsetY="0" OffsetZ="0"/>
					<ScaleTransform3D CenterX="0.000000" CenterY="0.000000" CenterZ="0.000000" ScaleX="1" ScaleY="1" ScaleZ="1"/>
					<RotateTransform3D CenterX="0.000000" CenterY="0.000000" CenterZ="0.000000">
						<RotateTransform3D.Rotation>
							<AxisAngleRotation3D Angle="0" Axis="0 1 0"/>
						</RotateTransform3D.Rotation>
					</RotateTransform3D>
					<TranslateTransform3D OffsetX="0" OffsetY="0" OffsetZ="0"/>
				</Transform3DGroup>
				<MeshGeometry3D x:Key="Geometry_test_obj1_0"
					TriangleIndices="2,1,0 3,2,0 5,4,1 2,5,1 7,6,4 5,7,4 3,0,6 7,3,6 1,4,6 0,1,6 5,2,3 7,5,3 "
					Positions="-160.219193,132.025406,117.890900 161.003204,132.025406,117.890900 202.421402,-139.093903,117.890800 -201.637405,-139.093903,117.890800 161.003204,132.025299,-117.890999 202.421402,-139.093994,-117.891098 -160.219193,132.025299,-117.890999 -201.637405,-139.093994,-117.891098 "
				/>
			</ResourceDictionary>
		</Viewport3D.Resources>

		<Viewport3D.Camera>
			<PerspectiveCamera x:Name="Camera_test" Position="955.806458,986.842163,1417.041260" LookDirection="-0.484275,-0.500000,-0.717968" UpDirection="0.000000,1.000000,0.000000" FieldOfView="20.000000" NearPlaneDistance="1" FarPlaneDistance="10000" />
		</Viewport3D.Camera>

		<ModelVisual3D>
			<ModelVisual3D.Content>
				<Model3DGroup>
					<AmbientLight Color="#646464" />
					<DirectionalLight x:Name="Light_test" Color="#FFFFFF" Direction="-0.484275,-0.500000,-0.717968" />
				</Model3DGroup>
			</ModelVisual3D.Content>
		</ModelVisual3D>
		<ModelVisual3D x:Name="Character_test_Character_0" Transform="{StaticResource Transform_Character_test_Character_0}">
			<ModelVisual3D.Content>
				<Model3DGroup>
					<Model3DGroup x:Name="Model_test_obj1" Transform="{StaticResource Transform_Model_test_obj1}">
						<GeometryModel3D x:Name="Geometry_Model_test_obj1_0" Geometry="{StaticResource Geometry_test_obj1_0}" Material="{StaticResource Material_test_NoMaterial}"/>
					</Model3DGroup>
				</Model3DGroup>
			</ModelVisual3D.Content>
		</ModelVisual3D>
	</Viewport3D>

	<Viewbox.Resources>
		<Storyboard Duration="Forever" FillBehavior="HoldEnd" BeginTime="0:0:0" x:Key="Animation_test_Animation_0">
		</Storyboard>
	</Viewbox.Resources>

	<Viewbox.Triggers>
		<EventTrigger RoutedEvent="Viewport3D.Loaded">
			<EventTrigger.Actions>
				<BeginStoryboard Storyboard="{StaticResource Animation_test_Animation_0}" />
			</EventTrigger.Actions>
		</EventTrigger>
	</Viewbox.Triggers>
</Viewbox>

ここまでコード0行。

2.3Dオブジェクトの操作

3Dオブジェクトではほぼ必須と言える回転・拡大縮小処理をビヘイビアとして用意。もっと完成度の高いビヘイビアが標準であるかと思ったのですが、見つかりませんでした。

 Wpf3DModelOperateBehavior.cs

namespace Wpf3DTestApp
{
    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Media.Media3D;
    using System.Windows.Interactivity;

    public class Wpf3DModelOperateBehavior : Behavior<Viewport3D>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            this.AssociatedObject.MouseWheel += AssociatedObject_MouseWheel;
            this.AssociatedObject.MouseMove += AssociatedObject_MouseMove;
            this.AssociatedObject.MouseDown += AssociatedObject_MouseDown;
            this.AssociatedObject.MouseUp += AssociatedObject_MouseUp;
        }

        protected override void OnDetaching()
        {
            this.AssociatedObject.MouseWheel += AssociatedObject_MouseWheel;
            this.AssociatedObject.MouseMove -= AssociatedObject_MouseMove;
            base.OnDetaching();
        }

        void AssociatedObject_MouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
        {
            var viewport = sender as Viewport3D;
            if (viewport == null){
                return;
            }
            var camera = viewport.Camera as PerspectiveCamera;
            if (camera == null){
                return;
            }
            camera.FieldOfView += e.Delta / 100;
        }

        bool dragging = false;
        Point startPoint;
        double startangle;
        void AssociatedObject_MouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            dragging = false;
        }

        void AssociatedObject_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
        {
            dragging = true;
            startPoint = e.GetPosition(sender as Viewport3D);
            var axisRot = GetRot(sender);
            if (axisRot == null)
            {
                return;
            }
            startangle = axisRot.Angle;
        }

        void AssociatedObject_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
        {
            if (!dragging){
                return;
            }
            var axisRot = GetRot(sender);
            if (axisRot == null){
                return;
            }
            axisRot.Angle = startangle + (e.GetPosition(sender as Viewport3D).X - startPoint.X);
        }

        private AxisAngleRotation3D GetRot(object sender)
        {
            var viewport = sender as Viewport3D;
            if (viewport == null)
            {
                return null;
            }
            var model = viewport.Children.First();
            if (model == null || !(model is ModelVisual3D))
            {
                return null;
            }
            var transform = model.Transform;
            if (transform == null)
            {
                return null;
            }
            RotateTransform3D rotate = null;
            if (transform is RotateTransform3D)
            {
                rotate = transform as RotateTransform3D;
            }
            if (transform is Transform3DGroup)
            {
                var transgroup = transform as Transform3DGroup;
                rotate = transgroup.Children.First(trans => trans is RotateTransform3D) as RotateTransform3D;
            }
            if (rotate == null)
            {
                return null;
            }
            return rotate.Rotation as AxisAngleRotation3D;
        }

    }
}

まずビヘイビアとは何か?はこちらを参照くださいませ。作る過程でわかったこととしては3Dの回転やスケーリングを汎用的なビヘイビアとして作るのは非常に難しそうということ。Viewport3Dから操作対象のオブジェクトネストが深いので、実際の変換を行える保証がない。3Dモデルの作り方も影響しそう。今回のビヘイビアを適用するならこんな感じ。

MainWindow.xaml(抜粋)

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:me="clr-namespace:Wpf3DTestApp"
        mc:Ignorable="d" x:Class="Wpf3DTestApp.MainWindow"
        Title="MainWindow" Height="350" Width="525">
	<Grid MouseWheel="Grid_MouseWheel" MouseUp="Grid_MouseUp" MouseDown="Grid_MouseDown" MouseMove="Grid_MouseMove">
		<Viewbox xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:c="http://schemas.openxmlformats.org/markup-compatibility/2006" x:Name="Viewbox_test" c:Ignorable="d" d:IsLocked="True">
            <Viewport3D x:Name="Viewport3D_test" Width="400" Height="300">
                <i:Interaction.Behaviors>
                    <me:Wpf3DModelOperateBehavior/>
                </i:Interaction.Behaviors>
            </Viewport3D>
		</Viewbox>
	</Grid>
</Window>

簡単な操作はこれでできるようになりました。

img