Click here to Skip to main content
15,867,453 members
Articles / Desktop Programming / WPF

D3dHost - MDX and WPF interoperability

Rate me:
Please Sign up or sign in to vote.
5.00/5 (7 votes)
13 Nov 2012CPOL5 min read 33.9K   2.6K   20   5
This article shows how we can render an interoperable MDX (Managed DirectX) scene, inside a WPF window.

Sample Image

Table of contents

Introduction

Windows Presentation Foundation (WPF) provides a set of elements for rendering 3D graphics. Those elements are good for designing 3D controls and rendering some simple 3D scenes. But if we want to render a more complicated scene, we may want to use a more low-level technique.

For rendering DirectX scenes using unmanaged code, we can use the D3DImage class, as explained in the Introduction to D3DImage article. But if we want to write the whole of our application using managed code only, we may want to use a framework that enables rendering DirectX scenes using managed code.

For rendering DirectX scenes using managed code, we have the Managed DirectX (MDX) framework. Using MDX, we can create a Device and render our scene using its methods. For rendering an MDX scene in a WPF window, we can either create a MDX device using the handle of the WPF window or create a MDX device using a Windows.Forms control that is hosted in a WPF window using a WindowsFormsHost.

Those techniques can be good for rendering a separated region of an MDX scene. But when we want to interact with other WPF elements, we find that some of the WPF effects (e.g., opacity, transactions, etc...) don't work as expected.

This article shows how we can render an MDX scene, inside a WPF window, as an interoperable WPF control.

How it works

Present the MDX scene 

Create a control for holding a MDX device

In order to support interoperability between WPF and MDX, we create a WPF custom control for hosting an MDX scene:

C#
public class D3dHost : Control
{
    static D3dHost()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(D3dHost), 
            new FrameworkPropertyMetadata(typeof(D3dHost)));
    }
}

In that control, we add a Windows.Forms Panel:

C#
#region D3dHostingPanel
private System.Windows.Forms.Panel _d3dHostingPanel;
protected System.Windows.Forms.Panel D3dHostingPanel
{
    get
    {
        if (_d3dHostingPanel == null)
        {
            _d3dHostingPanel = new System.Windows.Forms.Panel();

            int surfaceWidth = (int)D3dSurfaceWidth;
            int surfaceHeight = (int)D3dSurfaceHeight;

            _d3dHostingPanel.Width = (surfaceWidth > 0) ? surfaceWidth : 1;
            _d3dHostingPanel.Height = (surfaceHeight > 0) ? surfaceHeight : 1;
        }

        return _d3dHostingPanel;
    }
}
#endregion

#region D3dSurfaceWidth
public double D3dSurfaceWidth
{
    get { return (double)GetValue(D3dSurfaceWidthProperty); }
    set { SetValue(D3dSurfaceWidthProperty, value); }
}

public static readonly DependencyProperty D3dSurfaceWidthProperty =
    DependencyProperty.Register("D3dSurfaceWidth", typeof(double), typeof(D3dHost), 
        new UIPropertyMetadata(1000.0, OnD3dSurfaceWidthChanged));

private static void OnD3dSurfaceWidthChanged(DependencyObject sender,
    DependencyPropertyChangedEventArgs e)
{
    D3dHost dh = sender as D3dHost;
    if (dh == null)
    {
        return;
    }

    dh.UpdateDeviceWidth();
}

private void UpdateDeviceWidth()
{
    int surfaceWidth = (int)D3dSurfaceWidth;
    D3dHostingPanel.Width = (surfaceWidth > 0) ? surfaceWidth : 1;
}
#endregion

#region D3dSurfaceHeight
public double D3dSurfaceHeight
{
    get { return (double)GetValue(D3dSurfaceHeightProperty); }
    set { SetValue(D3dSurfaceHeightProperty, value); }
}

public static readonly DependencyProperty D3dSurfaceHeightProperty =
    DependencyProperty.Register("D3dSurfaceHeight", typeof(double), typeof(D3dHost), 
        new UIPropertyMetadata(1000.0, OnD3dSurfaceHeightChanged));

private static void OnD3dSurfaceHeightChanged(DependencyObject sender,
    DependencyPropertyChangedEventArgs e)
{
    D3dHost dh = sender as D3dHost;
    if (dh == null)
    {
        return;
    }

    dh.UpdateDeviceHeight();
}

private void UpdateDeviceHeight()
{
    int surfaceHeight = (int)D3dSurfaceHeight;
    D3dHostingPanel.Height = (surfaceHeight > 0) ? surfaceHeight : 1;
}
#endregion

and create an MDX Device using that Panel:

C#
#region D3dDevice       
private Device _d3dDevice;
public Device D3dDevice
{
    get 
    {
        if (_d3dDevice == null)
        {
            InitDevice();
        }

        return _d3dDevice; 
    }
}

protected void InitDevice()
{
    ReleaseDevice();

    PresentParameters presentParams = new PresentParameters();
    presentParams.Windowed = true;
    presentParams.SwapEffect = D3dSwapEffect;
    presentParams.EnableAutoDepthStencil = D3dEnableAutoDepthStencil;
    presentParams.AutoDepthStencilFormat = D3dAutoDepthStencilFormat; 

    _d3dDevice = new Device(0, D3dDeviceType, D3dHostingPanel, D3dCreateFlags, presentParams);
}

protected void ReleaseDevice()
{
    if (_d3dDevice != null)
    {
        _d3dDevice.Dispose();
        _d3dDevice = null;
    }
}
#endregion

#region D3dDeviceType
public DeviceType D3dDeviceType
{
    get { return (DeviceType)GetValue(D3dDeviceTypeProperty); }
    set { SetValue(D3dDeviceTypeProperty, value); }
}

public static readonly DependencyProperty D3dDeviceTypeProperty =
       DependencyProperty.Register("D3dDeviceType", typeof(DeviceType), typeof(D3dHost), 
       new UIPropertyMetadata(DeviceType.Hardware, OnD3dDeviceTypeChanged));

private static void OnD3dDeviceTypeChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    D3dHost dh = sender as D3dHost;
    if (dh == null)
    {
        return;
    }

    if (dh._d3dDevice != null)
    {
        // The device has been created with a different value. So, recreate it.
        dh.InitDevice();
    }
}
#endregion

#region D3dCreateFlags
public CreateFlags D3dCreateFlags
{
    get { return (CreateFlags)GetValue(D3dCreateFlagsProperty); }
    set { SetValue(D3dCreateFlagsProperty, value); }
}

public static readonly DependencyProperty D3dCreateFlagsProperty =
    DependencyProperty.Register("D3dCreateFlags", typeof(CreateFlags), typeof(D3dHost), 
        new UIPropertyMetadata(CreateFlags.SoftwareVertexProcessing, OnD3dCreateFlagsChanged));

private static void OnD3dCreateFlagsChanged(DependencyObject sender, 
        DependencyPropertyChangedEventArgs e)
{
    D3dHost dh = sender as D3dHost;
    if (dh == null)
    {
        return;
    }

    if (dh._d3dDevice != null)
    {
        // The device has been created with a different value. So, recreate it.
        dh.InitDevice();
    }
}        
#endregion

#region D3dSwapEffect
public SwapEffect D3dSwapEffect
{
    get { return (SwapEffect)GetValue(D3dSwapEffectProperty); }
    set { SetValue(D3dSwapEffectProperty, value); }
}

public static readonly DependencyProperty D3dSwapEffectProperty =
    DependencyProperty.Register("D3dSwapEffect", typeof(SwapEffect), typeof(D3dHost), 
        new UIPropertyMetadata(SwapEffect.Discard, OnD3dSwapEffectChanged));

private static void OnD3dSwapEffectChanged(DependencyObject sender, 
        DependencyPropertyChangedEventArgs e)
{
    D3dHost dh = sender as D3dHost;
    if (dh == null)
    {
        return;
    }

    if (dh._d3dDevice != null)
    {
        // The device has been created with a different value. So, recreate it.
        dh.InitDevice();
    }
}              
#endregion

#region D3dEnableAutoDepthStencil
public bool D3dEnableAutoDepthStencil
{
    get { return (bool)GetValue(D3dEnableAutoDepthStencilProperty); }
    set { SetValue(D3dEnableAutoDepthStencilProperty, value); }
}

public static readonly DependencyProperty D3dEnableAutoDepthStencilProperty =
    DependencyProperty.Register("D3dEnableAutoDepthStencil", typeof(bool), typeof(D3dHost),
        new UIPropertyMetadata(true, OnD3dEnableAutoDepthStencilChanged));

private static void OnD3dEnableAutoDepthStencilChanged(DependencyObject sender, 
                    DependencyPropertyChangedEventArgs e)
{
    D3dHost dh = sender as D3dHost;
    if (dh == null)
    {
        return;
    }

    if (dh._d3dDevice != null)
    {
        // The device has been created with a different value. So, recreate it.
        dh.InitDevice();
    }
}
#endregion

#region D3dAutoDepthStencilFormat
public DepthFormat D3dAutoDepthStencilFormat
{
    get { return (DepthFormat)GetValue(D3dAutoDepthStencilFormatProperty); }
    set { SetValue(D3dAutoDepthStencilFormatProperty, value); }
}

public static readonly DependencyProperty D3dAutoDepthStencilFormatProperty =
    DependencyProperty.Register("D3dAutoDepthStencilFormat", typeof(DepthFormat), typeof(D3dHost),
    new UIPropertyMetadata(DepthFormat.D16, OnD3dAutoDepthStencilFormatChanged));

private static void OnD3dAutoDepthStencilFormatChanged(DependencyObject sender, 
        DependencyPropertyChangedEventArgs e)
{
    D3dHost dh = sender as D3dHost;
    if (dh == null)
    {
        return;
    }

    if (dh._d3dDevice != null)
    {
        // The device has been created with a different value. So, recreate it.
        dh.InitDevice();
    }
}
#endregion

Create a region for presenting the MDX scene

For presenting the MDX scene on a WPF control, we add a TemplatePart for the region that presents the MDX scene:

C#
[TemplatePart(Name = "PART_D3dRegion", Type = typeof(Rectangle))]
public class D3dHost : Control
{
}

Create a default style that contains a Rectangle that is named with the TemplatePart's name:

XML
<Style TargetType="{x:Type local:D3dHost}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:D3dHost}">
                <Border Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <Rectangle Name="PART_D3dRegion" 
                                Stroke="Transparent" 
                                StrokeThickness="0" />
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

and, find the Rectangle according to the name:

C#
private Rectangle _d3dRegion;

public override void OnApplyTemplate()
{
    _d3dRegion = GetTemplateChild("PART_D3dRegion") as Rectangle;

    base.OnApplyTemplate();
}

Present the MDX device's surface

For presenting the MDX scene, we get a copy of the MDX surface and, set it as the Fill of the Rectangle. We can do that in the following ways:

  • Using a D3DImage:

    C#
    #region SetD3dRegionFillUsingD3DImage
    
    // First approach for setting the Fill of the 3D region, can be using a D3DImage.
    // For presenting a DirectX scene using a D3DImage,
    // we can set the D3DImage's back-buffer with the surface's pointer. 
    // But, If we set the D3DImage (that holds the surface's pointer) as the Fill of the 3D region,
    // every change to the surface, can be reflected directly to the Fill of the 3D region.
    // Therefore, in order to update the Fill of the 3D region,
    // only when the scene has been completely rendered,
    // we set the Fill of the 3D region with a copy of the D3Dimage's back-buffer.
    // Since the D3DImage.CopyBackBuffer method is protected,
    // we create another class for getting the copy of the back-buffer (D3DImageEx).
    
    protected class D3DImageEx : D3DImage
    {
        public D3DImageEx()
        {
        }
    
        public D3DImageEx(double width, double height)
            : base(width, height)
        {
        }
    
        public BitmapSource GetBackBufferCopy()
        {
            return CopyBackBuffer();
        }
    }
    
    private void SetD3dRegionFillUsingD3DImage(Surface s)
    {
        bool isD3dRegionUpdateNeeded = true;
    
        // Update the Fill of the 3D region, in the UI thread.
        Dispatcher.Invoke(
            new ThreadStart(() =>
            {
                try
                {
                    // Create a D3DImage that holds the surface's pointer.
                    D3dHost.D3DImageEx di = new D3dHost.D3DImageEx(D3dSurfaceWidth, D3dSurfaceHeight);
                    SetD3dImageBackBuffer(di, s);
    
                    // Set the Fill of the 3D region, to the D3DImage's back-buffer's copy.
                    BitmapSource bs = di.GetBackBufferCopy();
                    _d3dRegion.Fill = new ImageBrush(bs);
    
                    isD3dRegionUpdateNeeded = false;
    
                    _isSetD3dRegionFillUsingD3DImageSupported = true;
                }
                catch (Exception ex)
                {
                    if (_isSetD3dRegionFillUsingD3DImageSupported != true)
                    {
                        // The update using D3DImage isn't supported...
                        _isSetD3dRegionFillUsingD3DImageSupported = false;
                    }
                }
            }), TimeSpan.FromMilliseconds(MillisecondsForDispatcherInvokeTimeout));
    
        if (isD3dRegionUpdateNeeded && _continueUpdateD3dRegionThread)
        {
            InvalidateD3dRegion();
        }
    }
    
    private void SetD3dImageBackBuffer(D3DImage di, Surface s)
    {
        if (di == null || s == null)
        {
            return;
        }
    
        IntPtr backBuffer;
        unsafe
        {
            backBuffer = new IntPtr(s.UnmanagedComPointer);
        }
    
        di.Lock();
        di.SetBackBuffer(D3DResourceType.IDirect3DSurface9, backBuffer);
        di.AddDirtyRect(new Int32Rect(0, 0, di.PixelWidth,
            di.PixelHeight));
        di.Unlock();
    }
    
    #endregion
  • Using a buffer in the memory:

    C#
    #region SetD3dRegionFillUsingMemory
    
    // A second approach for setting the Fill of the 3D region, can be using a buffer in the memory.
    
    private GraphicsStream _d3dGraphicsStream;
    
    private void SetD3dRegionFillUsingMemory(Surface s)
    {
        GraphicsStream oldGraphicsStream = _d3dGraphicsStream;
    
        // Store the back-buffer as an image in the memory.
        GraphicsStream newGraphicsStream = SurfaceLoader.SaveToStream(ImageFileFormat.Bmp, s);
        newGraphicsStream.Seek(0, System.IO.SeekOrigin.Begin);
    
        lock (_d3dRegion)
        {
            _d3dGraphicsStream = newGraphicsStream;
        }
    
        // Update the Fill of the 3D region, in the UI thread.
        Dispatcher.BeginInvoke(new ThreadStart(() =>
            {
                lock (_d3dRegion)
                {
                    if (_continueUpdateD3dRegionThread)
                    {
                        try
                        {
                            // Create an ImageSource that contains the image of the back-buffer.
                            BitmapImage bi = new BitmapImage();
                            bi.BeginInit();
                            bi.StreamSource = _d3dGraphicsStream;
                            bi.EndInit();
    
                            // Set the Fill of the 3D region to the image of the back-buffer.
                            _d3dRegion.Fill = new ImageBrush(bi);
    
                            // The operation succeeded. So, it is supported.
                            _isSetD3dRegionFillUsingMemorySupported = true;
                        }
                        catch
                        {
                            if (_isSetD3dRegionFillUsingMemorySupported == true)
                            {
                                // There is a failure in the operation. But, it's supported.
                                // Maybe we have to free the memory of the unneeded GraphicsStream objects.
                                _isMemoryFreeNeeded = true;
                            }
                            else
                            {
                                // The update using the memory isn't supported...
                                _isSetD3dRegionFillUsingMemorySupported = false;
                            }
    
                            // This operation has failed. Give it another chance.
                            _updateD3dRegionEvent.Set();
                        }
                    }
                }
            }));
    
        ReleaseD3dGraphicsStream(oldGraphicsStream);
    }
    
    private void ReleaseD3dGraphicsStream(GraphicsStream d3dGraphicsStream)
    {
        if (d3dGraphicsStream != null)
        {
            d3dGraphicsStream.Close();
    
            // Extra Close ( http://www.eggheadcafe.com/microsoft/ 
            //    Win32-DirectX-Managed/31961917/
            //    surfaceloadersavetostream-major-memory-leak.aspx )
            d3dGraphicsStream.Close();
        }
    }
    
    protected void ReleaseD3dRegionMemory()
    {
        lock (_d3dRegion)
        {
            if (_d3dGraphicsStream != null)
            {
                ReleaseD3dGraphicsStream(_d3dGraphicsStream);
                _d3dGraphicsStream = null;
            }
        }
    }
    #endregion
  • Using a file on the disk:

    C#
    #region SetD3dRegionFillUsingFile
    
    // A third approach for setting the Fill of the 3D region, can be using an image file.
    
    private string _currentD3dRegionFillFileName;
    
    #region UsedFileNames
    private List<string> _usedFileNames;
    public List<string> UsedFileNames
    {
        get { return _usedFileNames ?? (_usedFileNames = new List<string>()); }
    }
    #endregion
    
    private void SetD3dRegionFillUsingFile(Surface s)
    {
        // Get available file name, for the storing the back-buffer.
        string currD3dRegionFillFileName = GetAvailableFileName();
               
        // Save the back-buffer as a file.
        SurfaceLoader.Save(currD3dRegionFillFileName, ImageFileFormat.Jpg, s);
    
        lock (_d3dRegion)
        {
            _currentD3dRegionFillFileName = currD3dRegionFillFileName;
    
            // Store the back-buffer file's name, in order to delete it later.
            UsedFileNames.Add(currD3dRegionFillFileName);
        }
    
        // Update the Fill of the 3D region, in the UI thread.
        Dispatcher.BeginInvoke(new ThreadStart(() =>
            {
                lock (_d3dRegion)
                {
                    if (_continueUpdateD3dRegionThread)
                    {
                        try
                        {
                            // Set the Fill of the 3D region to the saved back-buffer's file.
                            _d3dRegion.Fill =
                                new ImageBrush(new BitmapImage(
                                    new Uri(_currentD3dRegionFillFileName, UriKind.Relative)));
                        }
                        catch
                        {
                        }
                    }
                }
            }));
    
        // Delete the used files except the last one (we use it as the current Fill...).
        DeleteD3dRegionFiles(false);
    }
    
    private string GetAvailableFileName()
    {
        string fileNameBegin = "MdxWpf";
        string fileNameEnd = ".jpg";
    
        int fileNameCounter = 1;
    
        string currFileName = string.Format("{0}{1}{2}", 
            fileNameBegin, fileNameCounter.ToString(), fileNameEnd);
    
        while (File.Exists(currFileName) && fileNameCounter > 0)
        {
            fileNameCounter++;
    
            currFileName = string.Format("{0}{1}{2}",
                fileNameBegin, fileNameCounter.ToString(), fileNameEnd);
        }
    
        return currFileName;
    }
    
    protected void DeleteD3dRegionFiles(bool deleteLastFile)
    {
        string[] usedFileNamesCopy = null;
    
        lock (_d3dRegion)
        {
            usedFileNamesCopy = UsedFileNames.ToArray();
        }
    
        int filesCount = usedFileNamesCopy.Length;
    
        if (filesCount < 1)
        {
            return;
        }
    
        if (!deleteLastFile)
        {
            filesCount--;
        }
    
        for (int fileInx = 0; fileInx < filesCount; fileInx++)
        {
            string currFileName = usedFileNamesCopy[fileInx];
    
            try
            {
                if (File.Exists(currFileName))
                {
                    File.Delete(currFileName);
                }
    
                lock (_d3dRegion)
                {
                    UsedFileNames.Remove(currFileName);
                }
            }
            catch
            {
            }
        }
    }
    #endregion

Since, we don't want to block the UI in cases of heavy scenes, we update the 3D region, in another thread:

C#
#region TryUseD3DImageBeforeUsingMemory
public bool TryUseD3DImageBeforeUsingMemory { get; set; }
#endregion

#region TryUseMemoryBeforeUsingFilesSystem
public bool TryUseMemoryBeforeUsingFilesSystem { get; set; }
#endregion

#region FreeMemoryBeforeUpdateD3dRegion
public bool FreeMemoryBeforeUpdateD3dRegion { get; set; }
#endregion

#region MillisecondsForDispatcherInvokeTimeout
public double MillisecondsForDispatcherInvokeTimeout { get; set; }
#endregion

public void InvalidateD3dRegion()
{
    if (_d3dRegion == null)
    {
        return;
    }

    // Start the thread that updates the Fill of the 3D region, if it is needed.
    if (_updateD3dRegionThread == null)
    {
        StartUpdateD3dRegionThread();
    }

    // Indicate that the Fill of the 3D region is invalid.
    _updateD3dRegionEvent.Set();
}

#region UpdateD3dRegion

private Thread _updateD3dRegionThread = null;
private bool _continueUpdateD3dRegionThread;
private AutoResetEvent _updateD3dRegionEvent = new AutoResetEvent(false);
private bool _isMemoryFreeNeeded = false;
private bool? _isSetD3dRegionFillUsingD3DImageSupported = null;
private bool? _isSetD3dRegionFillUsingMemorySupported = null;

private void UpdateD3dRegion()
{
    if (_d3dRegion == null)
    {
        return;
    }

    //Lock the D3dHostingPanel, for waiting to the scene to be fully rendered.
    Monitor.Enter(D3dHostingPanel);

    if (FreeMemoryBeforeUpdateD3dRegion || _isMemoryFreeNeeded)
    {
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }

    try
    {
        // Get the device's back-buffer.
        Surface s = D3dDevice.GetBackBuffer(0, 0, BackBufferType.Mono);

        if (TryUseD3DImageBeforeUsingMemory && _isSetD3dRegionFillUsingD3DImageSupported != false)
        {
            SetD3dRegionFillUsingD3DImage(s);
        }
        else if (TryUseMemoryBeforeUsingFilesSystem && _isSetD3dRegionFillUsingMemorySupported != false)
        {
            SetD3dRegionFillUsingMemory(s);
        }
        else
        {
            SetD3dRegionFillUsingFile(s);
        }
    }
    catch
    {
    }
    finally
    {
        // Unlock the D3dHostingPanel, for letting the scene to be rendered.
        Monitor.Exit(D3dHostingPanel);
    }
}

private void StartUpdateD3dRegionThread()
{
    if (_updateD3dRegionThread == null)
    {
        _continueUpdateD3dRegionThread = true;

        _updateD3dRegionThread = new Thread(new ThreadStart(() =>
            {
                while (_continueUpdateD3dRegionThread)
                {
                    _updateD3dRegionEvent.WaitOne();

                    if (_continueUpdateD3dRegionThread)
                    {
                        UpdateD3dRegion();
                    }
                }
            }));

        _updateD3dRegionThread.Start();
    }
}

private void StopUpdateD3dRegionThread()
{
    if (_updateD3dRegionThread != null)
    {
        _continueUpdateD3dRegionThread = false;
        _updateD3dRegionEvent.Set();
        _updateD3dRegionThread.Join();
        _updateD3dRegionThread = null;
    }
}

#endregion

protected void ReleaseD3dRegion()
{
    StopUpdateD3dRegionThread();

    ReleaseD3dRegionMemory();

    DeleteD3dRegionFiles(true);
}

For ensuring that the scene is fully rendered before it is presented, we add methods for indicating the begin and the end of the drawing:

C#
public void BeginDrawing()
{
    // Lock the D3dHostingPanel, for ensuring that the scene is fully rendered.
    Monitor.Enter(D3dHostingPanel);
}

public void EndDrawing()
{
    // Unlock the D3dHostingPanel, for letting the scene to be presented.
    Monitor.Exit(D3dHostingPanel);
            
    // Present the scene.
    InvalidateD3dRegion();
}

Inform about the MDX region size changes

In order to inform about size changes of the region that hosts the MDX scene, we add properties for the region's actual width and height:

C#
#region D3dRegionActualWidth
public double D3dRegionActualWidth
{
    get { return (double)GetValue(D3dRegionActualWidthProperty); }
    private set { SetValue(D3dRegionActualWidthProperty, value); }
}

public static readonly DependencyProperty D3dRegionActualWidthProperty =
    DependencyProperty.Register("D3dRegionActualWidth", typeof(double), typeof(D3dHost), 
        new UIPropertyMetadata(0.0));
#endregion

#region D3dRegionActualHeight
public double D3dRegionActualHeight
{
    get { return (double)GetValue(D3dRegionActualHeightProperty); }
    private set { SetValue(D3dRegionActualHeightProperty, value); }
}

public static readonly DependencyProperty D3dRegionActualHeightProperty =
    DependencyProperty.Register("D3dRegionActualHeight", typeof(double), typeof(D3dHost), 
        new UIPropertyMetadata(0.0));
#endregion

and add a RoutedEvent that is raised every time the size of the MDX scene region is changed:

C#
#region D3dRegionSizeChanged
public static readonly RoutedEvent D3dRegionSizeChangedEvent = EventManager.RegisterRoutedEvent(
    "D3dRegionSizeChanged", RoutingStrategy.Bubble, typeof(SizeChangedEventHandler), typeof(D3dHost));

public event SizeChangedEventHandler D3dRegionSizeChanged
{
    add { AddHandler(D3dRegionSizeChangedEvent, value); }
    remove { RemoveHandler(D3dRegionSizeChangedEvent, value); }
}
#endregion

public override void OnApplyTemplate()
{
    _d3dRegion = GetTemplateChild("PART_D3dRegion") as Rectangle;

    if (_d3dRegion != null)
    {
        D3dRegionActualWidth = _d3dRegion.ActualWidth;
        D3dRegionActualHeight = _d3dRegion.ActualHeight;

        _d3dRegion.SizeChanged += (s, e) =>
            {
                D3dRegionActualWidth = _d3dRegion.ActualWidth;
                D3dRegionActualHeight = _d3dRegion.ActualHeight;

                // Raise the D3dRegionSizeChanged on the D3D region's size is changed.
                e.RoutedEvent = D3dHost.D3dRegionSizeChangedEvent;
                RaiseEvent(e);
            };
    }

    base.OnApplyTemplate();
}

Inform about mouse events

In order to inform about mouse-events that occur on the MDX scene, we create a RoutedEvent for each mouse-event (GotMouseCapture, LostMouseCapture, MouseEnter, MouseLeave, MouseMove, MouseDown, MouseLeftButtonDown, MouseLeftButtonUp, MouseRightButtonDown, MouseRightButtonUp, MouseUp, MouseWheel, PreviewMouseDown, PreviewMouseLeftButtonDown, PreviewMouseMove, PreviewMouseRightButtonDown, PreviewMouseRightButtonUp, PreviewMouseUp, and PreviewMouseWheel). For instance, here is the RoutedEvent for the MouseMove event:

C#
public static readonly RoutedEvent D3dSurfaceMouseMoveEvent = EventManager.RegisterRoutedEvent(
    "D3dSurfaceMouseMove", RoutingStrategy.Bubble, 
    typeof(D3dSurfaceMouseEventHandler), typeof(D3dHost));

public event D3dSurfaceMouseEventHandler D3dSurfaceMouseMove
{
    add { AddHandler(D3dSurfaceMouseMoveEvent, value); }
    remove { RemoveHandler(D3dSurfaceMouseMoveEvent, value); }
}

For raising the appropriate RoutedEvent with the MDX surface's mouse position, we get the surface's mouse position:

C#
private Point GetD3dSurfaceMousePosition(MouseEventArgs mouseArgs)
{
    // Get the mouse position on the 3D region.
    Point d3dRegionMousePosition = mouseArgs.GetPosition(_d3dRegion);

    // Calculate the mouse position on the MDX surface.
    Point d3dSurfaceMousePosition =
        new Point(d3dRegionMousePosition.X * D3dSurfaceWidth / D3dRegionActualWidth,
            d3dRegionMousePosition.Y * D3dSurfaceHeight / D3dRegionActualHeight);

    return d3dSurfaceMousePosition;
}

get the appropriate mouse-event:

C#
private RoutedEvent GetD3dSurfaceMouseEvent(MouseEventArgs mouseArgs)
{
    string d3dRegionEventName = mouseArgs.RoutedEvent.Name;

    string d3dSurfaceEventName;
    if (d3dRegionEventName.StartsWith("Preview"))
    {
        d3dSurfaceEventName = "PreviewD3dSurface" + d3dRegionEventName.Substring(7);
    }
    else
    {
        d3dSurfaceEventName = "D3dSurface" + d3dRegionEventName;
    }

    RoutedEvent d3dSurfaceMouseEvent =
        EventManager.GetRoutedEvents().FirstOrDefault(
            re => re.OwnerType == typeof(D3dHost) && re.Name == d3dSurfaceEventName);

    return d3dSurfaceMouseEvent;
}

and raise the appropriate mouse-event, in the event-handler of the original mouse-event:

C#
private void RegisterD3dRegionMouseEvents()
{
    ...
    _d3dRegion.MouseMove += OnD3dRegionMouseEvent;
    ...
}

private void OnD3dRegionMouseEvent(object sender, MouseEventArgs e)
{
    RoutedEvent d3dSurfaceMouseEvent = GetD3dSurfaceMouseEvent(e);

    if (d3dSurfaceMouseEvent != null)
    {
        D3dSurfaceMouseEventArgs d3dSurfaceEventArgs = 
            new D3dSurfaceMouseEventArgs(d3dSurfaceMouseEvent)
        {
            MouseEventArgs = e,
            D3dSurfaceMousePosition = GetD3dSurfaceMousePosition(e)
        };

        RaiseEvent(d3dSurfaceEventArgs);
    }
}

How to use it

Environment settings

Prevent the "LoaderLock was detected" popup window

In some cases, we may get a "LoaderLock was detected" popup while debugging the code. In order to stop it, we can choose the "Exceptions" option under the "Debug" menu and uncheck the "LoaderLock" item under the "Managed Debugging Assistants" group.

Set configuration to support "Mixed mode assembly"

For supporting the use of mixed mode assemblies on .NET 4, we can set the useLegacyV2RuntimeActivationPolicy attribute of the startup element of the application configuration to true, as follows:

XML
<configuration>
  <startup useLegacyV2RuntimeActivationPolicy="true">
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
  </startup>
</configuration>

Present a scene

For demonstrating the use of the D3dHost control for presenting a scene, we create a window that presents some rotating cones.

In order to compare the MDX 3D framework with the WPF 3D framework, we present the same scene using MDX and using WPF.

For presenting the scene, we add a Grid that contains a ContentControl for holding the scene and a Slider for determining the number of presented cones:

XML
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <TextBlock Name="txtHeader"
                FontSize="36"
                HorizontalAlignment="Center" />
    <Viewbox Grid.Row="1">
        <ContentControl Name="content3d"
                        Width="1000"
                        Height="1000"/>
    </Viewbox>
    <DockPanel Grid.Row="2" Margin="5,0,5,5">
        <TextBlock Text="Cones quantity: " DockPanel.Dock="Left" />
        <TextBlock Text=")" DockPanel.Dock="Right" />
        <TextBlock Name="txtConesQuantity" DockPanel.Dock="Right" />
        <TextBlock Text=" (" DockPanel.Dock="Right" />
        <Slider Name="conesSlider"
            Minimum="1" Maximum="200" Value="5" 
            ValueChanged="conesSlider_ValueChanged" />
    </DockPanel>
</Grid>

Create a class for holding the scene data:

C#
public class SceneData
{
    public SceneData()
    {
        CameraPosition = new Point3D(0, 0, 1000);
        FarPlaneDistance = 10000;
    }

    public Point3D CameraPosition { get; set; }

    public double FarPlaneDistance { get; set; }

    #region Cones
    private List<ConeData> _cones;
    public List<ConeData> Cones
    {
        get { return _cones ?? (_cones = new List<ConeData>()); }
    }
    #endregion
}

public class ConeData
{
    public double Height { get; set; }
    public double BaseRadius { get; set; }
    public Color MaterialColor { get; set; }
    public Point3D CenterPosition { get; set; }
    public double RotationX { get; set; }
    public double RotationZ { get; set; }
}

initialize the scene data to contain some cones, according to the value of the Slider:

C#
private SceneData _scene;

private void InitScene()
{
    lock (_scene)
    {
        _scene.Cones.Clear();

        Color[] colors =
            new Color[] { Colors.Red, Colors.Green, Colors.Blue,
                        Colors.Purple, Colors.Orange, Colors.DarkCyan };

        int numOfRows = (int)conesSlider.Value;
        int numOfColumns = (int)conesSlider.Value;

        double coneHeight = 300;
        double coneBaseRadius = 150;
        double conesDistance = 400;

        double cameraPositionZ =
            (Math.Cos(Math.PI / 8) / Math.Sin(Math.PI / 8)) *
                (conesDistance * ((double)numOfRows + 1) / 2);
        _scene.CameraPosition = new System.Windows.Media.Media3D.Point3D(0, 0, cameraPositionZ);
        _scene.FarPlaneDistance = cameraPositionZ + conesDistance;

        int colorIndexCounter = 0;
        for (int rowInx = 0; rowInx < numOfRows; rowInx++)
        {
            for (int colInx = 0; colInx < numOfColumns; colInx++)
            {
                double coneX = ((double)(numOfColumns - 1) / -2 + colInx) * conesDistance;
                double coneY = ((double)(numOfRows - 1) / -2 + rowInx) * conesDistance;

                _scene.Cones.Add(new ConeData
                {
                    Height = coneHeight,
                    BaseRadius = coneBaseRadius,
                    CenterPosition = new System.Windows.Media.Media3D.Point3D(coneX, coneY, 0),
                    MaterialColor = colors[colorIndexCounter % colors.Length]
                });

                colorIndexCounter++;
            }
        }

        txtConesQuantity.Text = _scene.Cones.Count.ToString();
    }
}

initialize the window according to the RenderType (this value is set using a parameter of the window's constructor):

C#
public enum RenderType
{
    MDX,
    WPF
}

private RenderType _renderType;

private D3dHost _mdxHost;
private Viewport3D _viewport3d;

private void InitWindow()
{
    if (_renderType == RenderType.WPF)
    {
        _viewport3d = new Viewport3D();
        content3d.Content = _viewport3d;
        txtHeader.Text = "WPF Scene";
    }
    else
    {
        _mdxHost = new D3dHost();
        content3d.Content = _mdxHost;
        txtHeader.Text = "MDX Scene";
    }
}

and create threads for updating the scene and for rendering the scene:

C#
private Thread _updateThread;
private Thread _renderThread;
private bool _continueUpdateThread;
private bool _continueRenderThread;

private void StartThreads()
{
    _continueUpdateThread = true;
    _updateThread = new Thread(new ThreadStart(() =>
        {
            while (_continueUpdateThread)
            {
                UpdateScene();

                Thread.Sleep(10);
            }
        }));
    _updateThread.Start();

    _continueRenderThread = true;
    _renderThread = new Thread(new ThreadStart(() =>
        {
            while (_continueRenderThread)
            {
                RenderScene();

                Thread.Sleep(100);
            }
        }));
    _renderThread.Start();
}

private void UpdateScene()
{
    Random rand = new Random(DateTime.Now.Millisecond);

    lock (_scene)
    {
        foreach (ConeData cd in _scene.Cones)
        {
            int currRotationAxis = rand.Next(2);
            if (currRotationAxis == 1)
            {
                cd.RotationZ += 1;
            }
            else
            {
                cd.RotationX += 1;
            }
        }
    }
}

private void RenderScene()
{
    if (_renderType == RenderType.WPF)
    {
        // Since we add elements to a Viewport3D (and it is a part of the UI),
        // we have to render the scene in the UI thread.
        Dispatcher.BeginInvoke(new ThreadStart(() =>
            {
                lock (_scene)
                {
                    WpfSceneRenderer.WpfRenderScene(_scene, _viewport3d);
                }
            }));
    }
    else
    {
        lock (_scene)
        {
            MdxSceneRenderer.MdxRenderScene(_scene, _mdxHost);
        }
    }
}

The RenderScene method calls either the MdxRenderScene method or the WpfRenderScene method, according to the RenderType. Here is the implementation of those methods:

MDX scene 


WPF scene

C#
public static void MdxRenderScene(SceneData scene, 
       D3dHost mdxHost)
{
    if (scene == null || mdxHost == null)
    {
        return;
    }

    mdxHost.BeginDrawing();

    Device device = mdxHost.D3dDevice;

    device.RenderState.ZBufferEnable = true;
    device.RenderState.Lighting = true;

    device.Clear(
        ClearFlags.Target |
            ClearFlags.ZBuffer,
            Color.White, 1.0f, 0);

    // Init camera matrices
    device.Transform.View =
        Matrix.LookAtLH(new Vector3(
                (float)scene.CameraPosition.X,
                (float)scene.CameraPosition.Y,
                (float)scene.CameraPosition.Z),
            new Vector3(0.0f, 0.0f, 0.0f),
            new Vector3(0.0f, 1.0f, 0.0f));
    device.Transform.Projection =
        Matrix.PerspectiveFovLH(
            (float)Math.PI / 4.0f, 1.0f, 1.0f,
            (float)scene.FarPlaneDistance);

    // Add directional light
    device.Lights[0].Type =
        LightType.Directional;
    device.Lights[0].Diffuse = Color.White;
    device.Lights[0].Direction =
        Vector3.Normalize(
            new Vector3(-1, -1, -1));
    device.Lights[0].Enabled = true;

    device.BeginScene();

    foreach (ConeData cone in scene.Cones)
    {
        MdxRenderCone(cone, device);
    }

    device.EndScene();

    mdxHost.EndDrawing();
}
C#
public static void WpfRenderScene(SceneData scene, 
       Viewport3D viewport3d)
{
    if (scene == null || viewport3d == null)
    {
        return;
    }

    viewport3d.Children.Clear();

    // Init camera matrices
    viewport3d.Camera = new PerspectiveCamera
    {
        Position = scene.CameraPosition,
        UpDirection = new Vector3D(0, 1, 0),
        FarPlaneDistance = scene.FarPlaneDistance
    };

    // Add directional light
    Vector3D lightDirection = 
        new Vector3D(-1, -1, -1);
    lightDirection.Normalize();
    ModelVisual3D dirlight = new ModelVisual3D
    {
        Content = new DirectionalLight(
            Colors.White, lightDirection)
    };
    viewport3d.Children.Add(dirlight);

    foreach (ConeData cone in scene.Cones)
    {
        WpfRenderCone(cone, viewport3d);
    }
}
C#
private static void MdxRenderCone(ConeData cone,
        Device device)
{
    float coneHeight = (float)cone.Height;
    float coneBaseRadius = 
        (float)cone.BaseRadius;

    // Create the cone's material
    Color col = Color.FromArgb(
        ColorToInt(cone.MaterialColor));
    Material mtrl = new Material();
    mtrl.Diffuse = col;
    device.Material = mtrl;

    // Create the cone's geometry

    int numOfPoints = (int)cone.BaseRadius;
    if (numOfPoints < 10)
    {
        numOfPoints = 10;
    }

    double partAngle = Math.PI * 2 / numOfPoints;

    // Create the vertices' collections.
    CustomVertex.PositionNormal[] bodyVertices =
        new CustomVertex.PositionNormal[
            numOfPoints + 2];
    CustomVertex.PositionNormal[] baseVertices =
        new CustomVertex.PositionNormal[
            numOfPoints + 2];

    // Set the top vertex.
    bodyVertices[0].Position =
        new Vector3(0, coneHeight / 2, 0);
    bodyVertices[0].Normal =
        new Vector3(0, 1, 0);

    // Set the base center vertex.
    baseVertices[0].Position =
        new Vector3(0, coneHeight / -2, 0);
    baseVertices[0].Normal =
        new Vector3(0, -1, 0);

    float bodyNormalY =
        (float)(Math.Sin(Math.PI -
            Math.Atan(coneHeight / 
                coneBaseRadius) * 2) *
        Math.Sqrt(coneHeight * coneHeight +
            coneBaseRadius * coneBaseRadius));

    for (int vertexInx = 0; 
            vertexInx <= numOfPoints; 
            vertexInx++)
    {
        double currAngle = 
            vertexInx * partAngle;
        float currX =
            (float)(coneBaseRadius * 
                Math.Cos(currAngle));
        float currZ =
            (float)(coneBaseRadius * 
                Math.Sin(currAngle));

        // Set current body vertex.
        bodyVertices[numOfPoints + 1 - vertexInx].Position =
            new Vector3(
                currX, coneHeight / -2, currZ);
        bodyVertices[numOfPoints + 1 - vertexInx].Normal =
            Vector3.Normalize(new Vector3(
                currX, bodyNormalY, currZ));

        // Set current base vertex.
        baseVertices[vertexInx + 1].Position =
            new Vector3(
                currX, coneHeight / -2, currZ);
        baseVertices[vertexInx + 1].Normal =
            new Vector3(0, -1, 0);
    }

    // Set the world matrix
    float rotateXRadians =
        (float)(cone.RotationX / 180 * Math.PI);
    float rotateZRadians =
        (float)(cone.RotationZ / 180 * Math.PI);
    device.Transform.World =
        Matrix.RotationX(rotateXRadians) *
        Matrix.RotationZ(rotateZRadians) *
        Matrix.Translation(new Vector3(
            (float)cone.CenterPosition.X,
            (float)cone.CenterPosition.Y,
            (float)cone.CenterPosition.Z));

    // Render the cone
    device.VertexFormat =
        CustomVertex.PositionNormal.Format;
    device.DrawUserPrimitives(
        PrimitiveType.TriangleFan,
        numOfPoints, bodyVertices);
    device.DrawUserPrimitives(
        PrimitiveType.TriangleFan,
        numOfPoints, baseVertices);
}

private static int ColorToInt(
    System.Windows.Media.Color color)
{
    return (int)color.A << 24 |
        (int)color.R << 16 |
        (int)color.G << 8 |
        (int)color.B;
}
C#
private static void WpfRenderCone(ConeData cone,
        Viewport3D viewport3d)
{
    // Create the cone's material
    DiffuseMaterial coneMaterial =
        new DiffuseMaterial(
            new SolidColorBrush(
                cone.MaterialColor));

    // Create the cone's geometry

    int numOfPoints = (int)cone.BaseRadius;
    if (numOfPoints < 10)
    {
        numOfPoints = 10;
    }

    double partAngle = Math.PI * 2 / numOfPoints;

    // Create the vertices' collections.
    MeshGeometry3D coneMesh = new MeshGeometry3D();
    coneMesh.Positions = new Point3DCollection();
    coneMesh.Normals = new Vector3DCollection();
    coneMesh.TriangleIndices = new Int32Collection();

    // Set the top vertex.
    coneMesh.Positions.Add(new Point3D(
        0, cone.Height / 2, 0));
    coneMesh.Normals.Add(new Vector3D(0, 1, 0));

    // Set the base center vertex.
    coneMesh.Positions.Add(new Point3D(
        0, cone.Height / -2, 0));
    coneMesh.Normals.Add(new Vector3D(0, -1, 0));

    double bodyNormalY =
        Math.Sin(Math.PI -
            Math.Atan(cone.Height / 
                cone.BaseRadius) * 2) *
        Math.Sqrt(cone.Height * cone.Height +
            cone.BaseRadius * cone.BaseRadius);

    for (int vertexInx = 0;
         vertexInx <= numOfPoints;
         vertexInx++)
    {
        double currAngle = vertexInx * partAngle;
        double currX =
            cone.BaseRadius * Math.Cos(currAngle);
        double currZ =
            cone.BaseRadius * Math.Sin(currAngle);

        // Set current body vertex.
        coneMesh.Positions.Add(new Point3D(
            currX, cone.Height / -2, currZ));
        Vector3D bodyNormal = new Vector3D(
            currX, bodyNormalY, currZ);
        bodyNormal.Normalize();
        coneMesh.Normals.Add(bodyNormal);

        // Set current base vertex.
        coneMesh.Positions.Add(new Point3D(
            currX, cone.Height / -2, currZ));
        coneMesh.Normals.Add(
            new Vector3D(0, -1, 0));

        // Set current body and base indices.
        if (vertexInx > 0)
        {
            // Set current body index.
            coneMesh.TriangleIndices.Add(0); // Top                    
            coneMesh.TriangleIndices.Add(
                (vertexInx + 1) * 2);
            coneMesh.TriangleIndices.Add(
                vertexInx * 2);

            // Set current base index.
            coneMesh.TriangleIndices.Add(1); // Base center                    
            coneMesh.TriangleIndices.Add(
                vertexInx * 2 + 1);
            coneMesh.TriangleIndices.Add(
                (vertexInx + 1) * 2 + 1);
        }
    }

    GeometryModel3D coneGeometry =
        new GeometryModel3D(
            coneMesh, coneMaterial);

    // Set the world matrix
    Transform3DGroup transGroup =
        new Transform3DGroup();
    transGroup.Children.Add(
        new RotateTransform3D(
            new AxisAngleRotation3D(
                new Vector3D(1, 0, 0),
                cone.RotationX)));
    transGroup.Children.Add(
        new RotateTransform3D(
            new AxisAngleRotation3D(
                new Vector3D(0, 0, 1),
                cone.RotationZ)));
    transGroup.Children.Add(
        new TranslateTransform3D(
            cone.CenterPosition.X,
            cone.CenterPosition.Y,
            cone.CenterPosition.Z));

    // Render the cone
    ModelVisual3D coneModel =
        new ModelVisual3D
        {
            Content = coneGeometry,
            Transform = transGroup
        };
    viewport3d.Children.Add(coneModel);
}

The result can be shown like the following:

MDX sceneWPF scene

Interact with WPF elements

For demonstrating the interoperability between MDX and WPF using the D3dHost control, we create a window that presents an interoperable MDX scene.

In that window, we add a D3dHost control for presenting the scene:

XML
<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition />
    </Grid.RowDefinitions>
       
    <Grid Grid.Row="1"
            Opacity="{Binding Value, ElementName=opacitySlider}">
        <Grid.LayoutTransform>
            <RotateTransform Angle="{Binding Value, ElementName=rotationSlider}" />
        </Grid.LayoutTransform>
        <ScrollViewer
            HorizontalScrollBarVisibility="Visible"
            VerticalScrollBarVisibility="Visible">
            <MdxWpfInteroperability:D3dHost x:Name="mdxHost"
                                            D3dSurfaceMouseLeave="mdxHost_D3dSurfaceMouseLeave"
                                            D3dSurfaceMouseMove="mdxHost_D3dSurfaceMouseMove"/>
        </ScrollViewer>           
    </Grid>

</Grid>

add a Border for enabling some effects:

XML
<ToggleButton Name="optionsToggle"
                Content="Options"
                VerticalAlignment="Bottom"
                HorizontalAlignment="Left" />

<Border Grid.Row="1"
        Visibility="{Binding IsChecked, ElementName=optionsToggle, 
                     Converter={StaticResource BooleanToVisibilityConverter}}"
        BorderBrush="DarkCyan"
        BorderThickness="2"
        Background="DarkBlue"
        TextElement.Foreground="Cyan"
        CornerRadius="5"
        Opacity="0.7"
        HorizontalAlignment="Left"
        VerticalAlignment="Top">
    <StackPanel Margin="5">
        <DockPanel>
            <TextBlock DockPanel.Dock="Left" Text="Opacity: " />
            <TextBlock DockPanel.Dock="Right" Text=")" />
            <TextBlock DockPanel.Dock="Right" Text="{Binding Value, ElementName=opacitySlider}" />
            <TextBlock DockPanel.Dock="Right" Text=" (" />
            <Slider x:Name="opacitySlider" Minimum="0" Maximum="1" Value="0.8"
                    HorizontalAlignment="Left"
                    Width="200"/>
        </DockPanel>
        <DockPanel>
            <TextBlock DockPanel.Dock="Left" Text="Rotation: " />
            <TextBlock DockPanel.Dock="Right" Text=")" />
            <TextBlock DockPanel.Dock="Right" Text=" degrees" />
            <TextBlock DockPanel.Dock="Right" Text="{Binding Value, ElementName=rotationSlider}" />
            <TextBlock DockPanel.Dock="Right" Text=" (" />
            <Slider x:Name="rotationSlider" Minimum="0" Maximum="360" Value="10"
                    HorizontalAlignment="Left"
                    Width="200"/>
        </DockPanel>
        <DockPanel>
            <TextBlock DockPanel.Dock="Left" Text="Zoom: " />
            <TextBlock DockPanel.Dock="Right" Text=")" />
            <TextBlock DockPanel.Dock="Right" Text="{Binding Value, ElementName=zoomSlider}" />
            <TextBlock DockPanel.Dock="Right" Text=" (" />
            <Slider x:Name="zoomSlider" Minimum="0.05" Maximum="1" Value="0.5"
                    ValueChanged="zoomSlider_ValueChanged"
                    HorizontalAlignment="Left"
                    Width="200"/>
        </DockPanel>
    </StackPanel>
</Border>

add a Border for presenting the surface's mouse position:

XML
<Border HorizontalAlignment="Right"
        BorderBrush="Green"
        BorderThickness="2"
        Background="DarkGreen"
        TextElement.Foreground="LightGreen"
        CornerRadius="5"
        Padding="5">
    <StackPanel Orientation="Horizontal">
        <TextBlock Text="Surface mouse position: " />
        <TextBlock Name="surfaceMousePosition"
            Text="Out of surface" />
    </StackPanel>
</Border>

and add a background for showing the opacity effect:

XML
<Grid.Resources>
    <Border x:Key="backgroundVisual"
        Background="LightGreen"
        Opacity="0.2"
        Padding="10">
        <TextBlock Text="MDX &amp; WPF Interoperability" 
                Foreground="DarkGreen"
                FontSize="32" />
    </Border>
</Grid.Resources>

<Grid.Background>
    <VisualBrush Visual="{StaticResource backgroundVisual}"
                    Viewport="0,0,0.33,0.2"
                    TileMode="Tile"/>
</Grid.Background>

For demonstrating mouse interoperability, we draw a circle and a text that presents the circle's center position:

C#
private void RenderScene()
{
    float circleCenterX = 200;
    float circleCenterY = 300;
    float circleRadius = 50;

    // Clear the surface.
    mdxHost.D3dDevice.Clear(Microsoft.DirectX.Direct3D.ClearFlags.Target, 
            ColorToInt(Colors.DarkGray), 1.0f, 0);

    // Draw a circle.
    Render2dCircle(circleCenterX, circleCenterY, circleRadius, 
                   Colors.Red, mdxHost.D3dDevice); // Stroke
    Render2dCircle(circleCenterX, circleCenterY, circleRadius - 3, 
                   Colors.DarkRed, mdxHost.D3dDevice); // Fill
    Render2dCircle(circleCenterX, circleCenterY, 5, Colors.Salmon, mdxHost.D3dDevice); // Center indication

    // Draw a text that presents the circle's center position.
    Render2dText(string.Format("Circle center: ({0},{1})", circleCenterX, circleCenterY),
        (int)(circleCenterX - circleRadius), (int)(circleCenterY + circleRadius + 10),
        36f, Colors.White, mdxHost.D3dDevice);

    // Present the scene on the D3dHost control.
    mdxHost.InvalidateD3dRegion();
}        

public void Render2dCircle(float centerX, float centerY, float radius, Color color,
    Microsoft.DirectX.Direct3D.Device device)
{
    int convertedColor = ColorToInt(color);

    int numOfPoints = (int)radius;
    if (numOfPoints < 10)
    {
        numOfPoints = 10;
    }

    Microsoft.DirectX.Direct3D.CustomVertex.TransformedColored[] vertices = 
        new Microsoft.DirectX.Direct3D.CustomVertex.TransformedColored[numOfPoints + 2];
    vertices[0].Position = new Microsoft.DirectX.Vector4(centerX, centerY, 0, 1.0f);
    vertices[0].Color = convertedColor;

    double partAngle = Math.PI * 2 / numOfPoints;

    for (int vertexInx = 0; vertexInx <= numOfPoints; vertexInx++)
    {
        double currAngle = vertexInx * partAngle;
        float currX = (float)(centerX + radius * Math.Cos(currAngle));
        float currY = (float)(centerY + radius * Math.Sin(currAngle));

        vertices[vertexInx + 1].Position = 
            new Microsoft.DirectX.Vector4(currX, currY, 0, 1.0f);
        vertices[vertexInx + 1].Color = convertedColor;
    }

    device.BeginScene();
    device.VertexFormat = Microsoft.DirectX.Direct3D.CustomVertex.TransformedColored.Format;
    device.DrawUserPrimitives(Microsoft.DirectX.Direct3D.PrimitiveType.TriangleFan, numOfPoints, vertices);
    device.EndScene();
}

public void Render2dText(string text, int x, int y, float fontSize, 
       Color color, Microsoft.DirectX.Direct3D.Device device)
{
    System.Drawing.Font systemfont = 
       new System.Drawing.Font("Arial", fontSize, System.Drawing.FontStyle.Regular);
    Microsoft.DirectX.Direct3D.Font d3dFont = 
       new Microsoft.DirectX.Direct3D.Font(mdxHost.D3dDevice, systemfont);

    device.BeginScene();

    d3dFont.DrawText(null, text, new System.Drawing.Point(x, y),
        System.Drawing.Color.FromArgb(ColorToInt(color)));

    device.EndScene();

    d3dFont.Dispose();
}

handle the D3dSurfaceMouseMove event to present the surface's mouse position, when the mouse cursor is on the surface:

C#
private void mdxHost_D3dSurfaceMouseMove(object sender, D3dSurfaceMouseEventArgs e)
{
    surfaceMousePosition.Text = string.Format("({0},{1})",
        e.D3dSurfaceMousePosition.X, e.D3dSurfaceMousePosition.Y);
}

and handle the D3dSurfaceMouseLeave event to present "Out of surface", when the mouse cursor isn't on the surface:

C#
private void mdxHost_D3dSurfaceMouseLeave(object sender, D3dSurfaceMouseEventArgs e)
{
    surfaceMousePosition.Text = "Out of surface";
}

The result can be shown like the following:

Interoperability example

History

  • 23 March 2012 - Initial version.
  • Current - Addition of an option (the default option) to present the MDX scene using a D3DImage.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer
Israel Israel
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionHung Thread Pin
jay50262-Jan-13 6:16
jay50262-Jan-13 6:16 
First of all, thank you! I had been looking for a WPF control to host DirectX content and finally came across your tutorial. This is exactly what I had been searching all over for. Fantastic tutorial too.

I found when trying to implement this into a dummy project I created that the _updateD3dRegionThread was hanging after closing the window, causing the program to never actually exit. Eventually, I figured out that in my project I was implementing the host control in the MainWindow. When I created a separate window and had a button open an instance of the new window, the program would exit fine just as in your project.

I assume that when the new window is closed (works with both .Show() and .ShowDialog(), FYI) it is forcing all of the threads to exit. I'm just trying to understand this concept completely and was wondering if you have any insight as to why the thread gets stuck? I believe I tracked it to _updateD3dRegionThread.WaitOne(), but I'm not 100% sure.

Again, no big deal here. I got the concept to work by putting the control in a separate window. Just trying to make sure I completely understand the inner workings of your tutorial. Thanks!
AnswerRe: Hung Thread Pin
Shmuel Zang6-Jan-13 7:18
Shmuel Zang6-Jan-13 7:18 
GeneralRe: Hung Thread Pin
jay502611-Jan-13 7:15
jay502611-Jan-13 7:15 
QuestionI disagree Pin
Steve Maier23-Mar-12 8:41
professionalSteve Maier23-Mar-12 8:41 
AnswerRe: I disagree Pin
Shmuel Zang24-Mar-12 10:11
Shmuel Zang24-Mar-12 10:11 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.