Tristan Root's Blog

Archive for February, 2010

Wpf MVVM 3D Model Viewer – More View Panels

Posted by triroot on February 18, 2010

Introduction

In a previous post I showed you how to create a basic 3D model viewer in WPF using the MVVM pattern. In this post I’m going to show you how to take advantage of the MVVM design of this application by adding a couple more views (one view to move the camera, and one to change the mesh being viewed) and there will be one more view-model (for toggling the viewed mesh).

The Camera  View 

Here is the xaml for the Camera Control Panel, this will use the CameraVM as its DataContext.

<UserControl x:Class="TriRoot.Views.CameraPanel"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

   

    <StackPanel>

        <DockPanel>

            <TextBlock Text="Rotation:" Width="65" />

            <TextBlock DockPanel.Dock="Right" Width="65"

                Text="{Binding Rotation}" />

            <Slider x:Name="RotationSlider"

                Minimum="0" Maximum="6.283"

                Value="{Binding Rotation}"/>

        </DockPanel>

        <DockPanel >

            <TextBlock Text="Up / Down:" Width="65" />

            <TextBlock DockPanel.Dock="Right" Width="65"

                Text="{Binding UpPercent}" />

            <Slider x:Name="UpPercentSlider"

                Minimum="-1" Maximum="1"

                Value="{Binding UpPercent}"/>

        </DockPanel>

    </StackPanel>

   

</UserControl>

There is no additional code behind for this panel or the CameraVM. The sliders will update the values in the CameraVM through bindings when the user interacts with the sliders.

The following is an update to the test window to use this new panel. I’ve grayed out any code that was from the first post.

<Window x:Class="TriRoot.Test.TestWindow"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:views="clr-namespace:TriRoot.Views;assembly=TriRoot.Blog"

    xmlns:viewmodels="clr-namespace:TriRoot.ViewModels;assembly=TriRoot.Blog"

    Title="TriRoot.Test" Height="300" Width="300">

   

    <Window.Resources>

        <DataTemplate DataType="{x:Type viewmodels:ViewportVM}">

            <views:ViewportPanel />

        </DataTemplate>

        <DataTemplate DataType="{x:Type viewmodels:CameraVM}">

            <views:CameraPanel />

        </DataTemplate>

    </Window.Resources>

 

    <DockPanel>

        <ContentPresenter x:Name="CameraVMContentPresenter" DockPanel.Dock="Top"/>

        <ContentPresenter x:Name="ViewportVMContentPresenter" />       

    </DockPanel>

 

</Window>

 

And the CameraVM needs to be set as the Content of the new content presenter in the code behind 

CameraVM cameraVM = new CameraVM() { CameraData = cameraData };

MeshVM meshVM = new MeshVM() { MeshData = meshData };

ViewportVM viewportVM = new ViewportVM()

{

    Camera = cameraVM,

    Mesh = meshVM

};

 

CameraVMContentPresenter.Content = cameraVM;

ViewportVMContentPresenter.Content = viewportVM;

Here’s a screenshot of the result:

In another post I have the details on how to implement a simple Cube Mesh against the IMeshData interface. The resulting class is the MeshDataCube. Next I’m going to write a view model and view that allows the user to select between viewing a simple triangle or the MeshDataCube. 

The Mesh Editor View-Model

The mesh editor will have a couple commands for toggling the mesh in use. These commands get bound to buttons, so that when a user clicks the button, ICommand.Execute gets called. The DelegateCommand is an implementation of ICommand that takes an Action that gets invoked in DelegateCommand.Execute, and a Func<bool> that gets invoked and returned in DelegateCommand.CanExecute.  In this implementation, I’m ignoring the parameters of Execute and CanExecute.

public class DelegateCommand : ICommand

{

    Action _executeAction;

    Func<bool> _canExecuteFunc;

 

    public DelegateCommand(Action executeAction)

        : this(executeAction, null)

    {

    }

 

    public DelegateCommand(Action executeAction, Func<bool> canExecuteFunc)

    {

        _executeAction = executeAction;

        _canExecuteFunc = canExecuteFunc;

    }

 

    public event EventHandler CanExecuteChanged;

 

    public void Execute(object unused)

    {

        _executeAction();

    }

 

    public bool CanExecute(object unused)

    {

        return _canExecuteFunc != null ? _canExecuteFunc() : true;

    }

 

    public void FireCanExecuteChanged()

    {

        if (CanExecuteChanged != null)

        {

            CanExecuteChanged(this, new EventArgs());

        }

    }

}

The MeshEditor has two commands, one for switching to a triangle mesh, and one for switching to a cube mesh. The SelectedMesh property is a reference to the MeshVM which contains the MeshData that will be altered when a command executes.

public class MeshEditorVM : DependencyObject

{

    DelegateCommand _triangleCommand;

    DelegateCommand _cubeCommand;

 

    public MeshEditorVM()

    {

        _triangleCommand = new DelegateCommand(

            MakeTriangle,

            CanMakeTriangle);

 

        _cubeCommand = new DelegateCommand(

            MakeCube,

            CanMakeCube);

    }

 

    #region MeshVM SelectedMesh (DependencyProperty w/OnChanged)

    public static readonly DependencyProperty SelectedMeshDataProperty =

        DependencyProperty.Register(

            "SelectedMesh",

            typeof(MeshVM),

            typeof(MeshEditorVM),

            new FrameworkPropertyMetadata(OnSelectedMeshChanged));

 

    public MeshVM SelectedMesh

    {

        get { return (MeshVM)GetValue(SelectedMeshDataProperty); }

        set { SetValue(SelectedMeshDataProperty, value); }

    }

 

    protected static void OnSelectedMeshChanged(

        DependencyObject sender,

        DependencyPropertyChangedEventArgs args)

    {

        MeshEditorVM me = (MeshEditorVM)sender;

        me.RefreshCommands();

    }

    #endregion

 

    public ICommand TriangleCommand { get { return _triangleCommand; } }

    public ICommand CubeCommand { get { return _cubeCommand; } }

 

    private void RefreshCommands()

    {

        _triangleCommand.FireCanExecuteChanged();

        _cubeCommand.FireCanExecuteChanged();

    }

 

    private void MakeTriangle()

    {

        if (SelectedMesh != null)

        {

            SelectedMesh.MeshData = new MeshDataTriangle(

                new Point3D(0.5, 0, 0),

                new Point3D(0, 0.5, 0),

                new Point3D(0, 0, 0.5));

 

            RefreshCommands();

        }

    }

 

    private bool CanMakeTriangle()

    {

        return SelectedMesh != null

            && !(SelectedMesh.MeshData is MeshDataTriangle);

    }

 

    private void MakeCube()

    {

        if (SelectedMesh != null)

        {

            SelectedMesh.MeshData = new MeshDataCube(1.0);

            RefreshCommands();

        }

    }

 

    private bool CanMakeCube()

    {

        return SelectedMesh != null

            && !(SelectedMesh.MeshData is MeshDataCube);

    }

}

Here is the xaml for the panel that will use the MeshEditorVM as it s DataContext.

<UserControl x:Class="TriRoot.Views.MeshEditorPanel"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

   

    <StackPanel Background="LemonChiffon" Margin="2">

        <Button Content="Triangle" Command="{Binding TriangleCommand}" Margin="2"/>

        <Button Content="Cube" Command="{Binding CubeCommand}" Margin="2"/>

   </StackPanel>

   

</UserControl>

Again, the following is an update to the test window to use this new panel.

<Window x:Class="TriRoot.Test.TestWindow"

    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:views="clr-namespace:TriRoot.Views;assembly=TriRoot.Blog"

    xmlns:viewmodels="clr-namespace:TriRoot.ViewModels;assembly=TriRoot.Blog"

    Title="TriRoot.Test" Height="300" Width="300">

   

    <Window.Resources>

        <DataTemplate DataType="{x:Type viewmodels:ViewportVM}">

            <views:ViewportPanel />

        </DataTemplate>

        <DataTemplate DataType="{x:Type viewmodels:CameraVM}">

            <views:CameraPanel />

        </DataTemplate>

        <DataTemplate DataType="{x:Type viewmodels:MeshEditorVM}">

            <views:MeshEditorPanel />

        </DataTemplate>

    </Window.Resources>

 

    <DockPanel>

        <ContentPresenter x:Name="MeshEditorVMContentPresenter" DockPanel.Dock="Right"/>

        <ContentPresenter x:Name="CameraVMContentPresenter" DockPanel.Dock="Top"/>

        <ContentPresenter x:Name="ViewportVMContentPresenter" />       

    </DockPanel>

 

</Window>

And then in the code behind a MeshEditorVM gets hooked up to the new ContentPresenter.

ICameraData cameraData = new CameraData();

IMeshData meshData = new MeshDataTriangle(

    new Point3D(0.5, 0, 0),

    new Point3D(0, 0.5, 0),

    new Point3D(0, 0, 0.5));

 

CameraVM cameraVM = new CameraVM() { CameraData = cameraData };

MeshVM meshVM = new MeshVM() { MeshData = meshData };

ViewportVM viewportVM = new ViewportVM()

{

    Camera = cameraVM,

    Mesh = meshVM

};

 

MeshEditorVM meshEditorVM = new MeshEditorVM()

{

    SelectedMesh = meshVM

};

 

MeshEditorVMContentPresenter.Content = meshEditorVM;

CameraVMContentPresenter.Content = cameraVM;

ViewportVMContentPresenter.Content = viewportVM;

 

And in case you missed it from my first post, here’s the final screenshot:

BAM!

Posted in WPF | Tagged: , , , | Leave a Comment »

Wpf MVVM 3D Model Viewer – IMeshData Cube

Posted by triroot on February 16, 2010

In my last post I implemented a 3D Model Viewer that renders geometry from IMeshData. In the post I just rendered a simple triangle mesh. Here I’m going to implement a simple cube mesh. This implementation is designed to be easy to follow, and has not been optimized for saving memory.

For reference, here is what the IMeshData looks like:

public interface IMeshData

{

    IEnumerable<Point3D> Vertices { get; }

    IEnumerable<int> TriangleIndices { get; }

}

I implemented a static Utility class that will describe the data needed for each cube face.  The Contributions are the four corner points in 3D space for a face, and the TriangleIndices describes how to use those points to construct two triangles for the face

public enum CubeFaceType

{

    None,

    Left,

    Right,

    Top,

    Bottom,

    Front,

    Back,

}

class CubeFaceUtil

{

    internal static CubeFaceType[] GetCubeFaces()

    {

        return new CubeFaceType[]

        {

            CubeFaceType.Left,

            CubeFaceType.Right,

            CubeFaceType.Top,

            CubeFaceType.Bottom,

            CubeFaceType.Front,

            CubeFaceType.Back

        };

    }

 

    internal static Point3D[] GetContributions(CubeFaceType type)

    {

        switch (type)

        {

            case CubeFaceType.Left:

                return new Point3D[]

                {

                    new Point3D(-1, 1, -1),

                    new Point3D(-1, 1, 1),

                    new Point3D(-1, -1, 1),

                    new Point3D(-1, -1, -1)

                };

            case CubeFaceType.Right:

                return new Point3D[]

                {

                    new Point3D(1, 1, 1),

                    new Point3D(1, 1, -1),

                    new Point3D(1, -1, -1),

                    new Point3D(1, -1, 1)

                };

            case CubeFaceType.Top:

                return new Point3D[]

                {

                    new Point3D(-1, 1, -1),

                    new Point3D(1, 1, -1),

                    new Point3D(1, 1, 1),

                    new Point3D(-1, 1, 1)

                };

            case CubeFaceType.Bottom:

                return new Point3D[]

                {

                    new Point3D(1, -1, -1),

                    new Point3D(-1, -1, -1),

                    new Point3D(-1, -1, 1),

                    new Point3D(1, -1, 1)

                };

            case CubeFaceType.Front:

                return new Point3D[]

                {

                    new Point3D(-1, 1, 1),

                    new Point3D(1, 1, 1),

                    new Point3D(1, -1, 1),

                    new Point3D(-1, -1, 1)

                };

            case CubeFaceType.Back:

                return new Point3D[]

                {

                    new Point3D(1, 1, -1),

                    new Point3D(-1, 1, -1),

                    new Point3D(-1, -1, -1),

                    new Point3D(1, -1, -1)

                };

            default:

                throw new ArgumentException(

                    "Unsupported CubeFaceType",

                    "type");

        }

    }

 

    internal static int[] GetTriangleIndices()

    {

        return new int[]

            {

                0, 3, 1,

                1, 3, 2

            };

    }

}

Now all that needs to be done is to iterate through the faces and add them to the MeshDataCube.

public class MeshDataCube : IMeshData

{

    public MeshDataCube(double cubeSize)

    {

        double halfFaceSize = cubeSize / 2;

 

        List<int> allIndices = new List<int>();

        List<Point3D> allVertices = new List<Point3D>();

 

        foreach (CubeFaceType faceType in CubeFaceUtil.GetCubeFaces())

        {

            int[] indices = CubeFaceUtil.GetTriangleIndices();

            Point3D[] contributions = CubeFaceUtil.GetContributions(faceType);

            Point3D[] positions = contributions.Select(p => p.Scale(halfFaceSize)).ToArray();

 

            // shift the indicies by the number of vertices already added.

            allIndices.AddRange(indices.Select(i => i + allVertices.Count()));

            allVertices.AddRange(positions);

        }

 

        Vertices = allVertices;

        TriangleIndices = allIndices;

    }

 

    public IEnumerable<int> TriangleIndices { get; protected set; }

    public IEnumerable<Point3D> Vertices { get; protected set; }

}

BAM!

 

Posted in WPF | Tagged: , , | 1 Comment »

 
Follow

Get every new post delivered to your Inbox.