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:
public class D3dHost : Control
{
static D3dHost()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(D3dHost),
new FrameworkPropertyMetadata(typeof(D3dHost)));
}
}
In that control, we add a Windows.Forms
Panel
:
#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
:
#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)
{
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)
{
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)
{
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)
{
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)
{
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:
[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:
<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:
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
:
#region SetD3dRegionFillUsingD3DImage
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;
Dispatcher.Invoke(
new ThreadStart(() =>
{
try
{
D3dHost.D3DImageEx di = new D3dHost.D3DImageEx(D3dSurfaceWidth, D3dSurfaceHeight);
SetD3dImageBackBuffer(di, s);
BitmapSource bs = di.GetBackBufferCopy();
_d3dRegion.Fill = new ImageBrush(bs);
isD3dRegionUpdateNeeded = false;
_isSetD3dRegionFillUsingD3DImageSupported = true;
}
catch (Exception ex)
{
if (_isSetD3dRegionFillUsingD3DImageSupported != true)
{
_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:
#region SetD3dRegionFillUsingMemory
private GraphicsStream _d3dGraphicsStream;
private void SetD3dRegionFillUsingMemory(Surface s)
{
GraphicsStream oldGraphicsStream = _d3dGraphicsStream;
GraphicsStream newGraphicsStream = SurfaceLoader.SaveToStream(ImageFileFormat.Bmp, s);
newGraphicsStream.Seek(0, System.IO.SeekOrigin.Begin);
lock (_d3dRegion)
{
_d3dGraphicsStream = newGraphicsStream;
}
Dispatcher.BeginInvoke(new ThreadStart(() =>
{
lock (_d3dRegion)
{
if (_continueUpdateD3dRegionThread)
{
try
{
BitmapImage bi = new BitmapImage();
bi.BeginInit();
bi.StreamSource = _d3dGraphicsStream;
bi.EndInit();
_d3dRegion.Fill = new ImageBrush(bi);
_isSetD3dRegionFillUsingMemorySupported = true;
}
catch
{
if (_isSetD3dRegionFillUsingMemorySupported == true)
{
_isMemoryFreeNeeded = true;
}
else
{
_isSetD3dRegionFillUsingMemorySupported = false;
}
_updateD3dRegionEvent.Set();
}
}
}
}));
ReleaseD3dGraphicsStream(oldGraphicsStream);
}
private void ReleaseD3dGraphicsStream(GraphicsStream d3dGraphicsStream)
{
if (d3dGraphicsStream != null)
{
d3dGraphicsStream.Close();
d3dGraphicsStream.Close();
}
}
protected void ReleaseD3dRegionMemory()
{
lock (_d3dRegion)
{
if (_d3dGraphicsStream != null)
{
ReleaseD3dGraphicsStream(_d3dGraphicsStream);
_d3dGraphicsStream = null;
}
}
}
#endregion
Using a file on the disk:
#region SetD3dRegionFillUsingFile
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)
{
string currD3dRegionFillFileName = GetAvailableFileName();
SurfaceLoader.Save(currD3dRegionFillFileName, ImageFileFormat.Jpg, s);
lock (_d3dRegion)
{
_currentD3dRegionFillFileName = currD3dRegionFillFileName;
UsedFileNames.Add(currD3dRegionFillFileName);
}
Dispatcher.BeginInvoke(new ThreadStart(() =>
{
lock (_d3dRegion)
{
if (_continueUpdateD3dRegionThread)
{
try
{
_d3dRegion.Fill =
new ImageBrush(new BitmapImage(
new Uri(_currentD3dRegionFillFileName, UriKind.Relative)));
}
catch
{
}
}
}
}));
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:
#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;
}
if (_updateD3dRegionThread == null)
{
StartUpdateD3dRegionThread();
}
_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;
}
Monitor.Enter(D3dHostingPanel);
if (FreeMemoryBeforeUpdateD3dRegion || _isMemoryFreeNeeded)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
try
{
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
{
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:
public void BeginDrawing()
{
Monitor.Enter(D3dHostingPanel);
}
public void EndDrawing()
{
Monitor.Exit(D3dHostingPanel);
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:
#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:
#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;
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:
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:
private Point GetD3dSurfaceMousePosition(MouseEventArgs mouseArgs)
{
Point d3dRegionMousePosition = mouseArgs.GetPosition(_d3dRegion);
Point d3dSurfaceMousePosition =
new Point(d3dRegionMousePosition.X * D3dSurfaceWidth / D3dRegionActualWidth,
d3dRegionMousePosition.Y * D3dSurfaceHeight / D3dRegionActualHeight);
return d3dSurfaceMousePosition;
}
get the appropriate mouse-event:
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:
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:
<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:
<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:
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
:
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):
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:
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)
{
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 |
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);
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);
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();
} |
public static void WpfRenderScene(SceneData scene,
Viewport3D viewport3d)
{
if (scene == null || viewport3d == null)
{
return;
}
viewport3d.Children.Clear();
viewport3d.Camera = new PerspectiveCamera
{
Position = scene.CameraPosition,
UpDirection = new Vector3D(0, 1, 0),
FarPlaneDistance = scene.FarPlaneDistance
};
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);
}
} |
private static void MdxRenderCone(ConeData cone,
Device device)
{
float coneHeight = (float)cone.Height;
float coneBaseRadius =
(float)cone.BaseRadius;
Color col = Color.FromArgb(
ColorToInt(cone.MaterialColor));
Material mtrl = new Material();
mtrl.Diffuse = col;
device.Material = mtrl;
int numOfPoints = (int)cone.BaseRadius;
if (numOfPoints < 10)
{
numOfPoints = 10;
}
double partAngle = Math.PI * 2 / numOfPoints;
CustomVertex.PositionNormal[] bodyVertices =
new CustomVertex.PositionNormal[
numOfPoints + 2];
CustomVertex.PositionNormal[] baseVertices =
new CustomVertex.PositionNormal[
numOfPoints + 2];
bodyVertices[0].Position =
new Vector3(0, coneHeight / 2, 0);
bodyVertices[0].Normal =
new Vector3(0, 1, 0);
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));
bodyVertices[numOfPoints + 1 - vertexInx].Position =
new Vector3(
currX, coneHeight / -2, currZ);
bodyVertices[numOfPoints + 1 - vertexInx].Normal =
Vector3.Normalize(new Vector3(
currX, bodyNormalY, currZ));
baseVertices[vertexInx + 1].Position =
new Vector3(
currX, coneHeight / -2, currZ);
baseVertices[vertexInx + 1].Normal =
new Vector3(0, -1, 0);
}
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));
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;
} |
private static void WpfRenderCone(ConeData cone,
Viewport3D viewport3d)
{
DiffuseMaterial coneMaterial =
new DiffuseMaterial(
new SolidColorBrush(
cone.MaterialColor));
int numOfPoints = (int)cone.BaseRadius;
if (numOfPoints < 10)
{
numOfPoints = 10;
}
double partAngle = Math.PI * 2 / numOfPoints;
MeshGeometry3D coneMesh = new MeshGeometry3D();
coneMesh.Positions = new Point3DCollection();
coneMesh.Normals = new Vector3DCollection();
coneMesh.TriangleIndices = new Int32Collection();
coneMesh.Positions.Add(new Point3D(
0, cone.Height / 2, 0));
coneMesh.Normals.Add(new Vector3D(0, 1, 0));
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);
coneMesh.Positions.Add(new Point3D(
currX, cone.Height / -2, currZ));
Vector3D bodyNormal = new Vector3D(
currX, bodyNormalY, currZ);
bodyNormal.Normalize();
coneMesh.Normals.Add(bodyNormal);
coneMesh.Positions.Add(new Point3D(
currX, cone.Height / -2, currZ));
coneMesh.Normals.Add(
new Vector3D(0, -1, 0));
if (vertexInx > 0)
{
coneMesh.TriangleIndices.Add(0); coneMesh.TriangleIndices.Add(
(vertexInx + 1) * 2);
coneMesh.TriangleIndices.Add(
vertexInx * 2);
coneMesh.TriangleIndices.Add(1); coneMesh.TriangleIndices.Add(
vertexInx * 2 + 1);
coneMesh.TriangleIndices.Add(
(vertexInx + 1) * 2 + 1);
}
}
GeometryModel3D coneGeometry =
new GeometryModel3D(
coneMesh, coneMaterial);
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));
ModelVisual3D coneModel =
new ModelVisual3D
{
Content = coneGeometry,
Transform = transGroup
};
viewport3d.Children.Add(coneModel);
} |
The result can be shown like the following:
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:
<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:
<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:
<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:
<Grid.Resources>
<Border x:Key="backgroundVisual"
Background="LightGreen"
Opacity="0.2"
Padding="10">
<TextBlock Text="MDX & 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:
private void RenderScene()
{
float circleCenterX = 200;
float circleCenterY = 300;
float circleRadius = 50;
mdxHost.D3dDevice.Clear(Microsoft.DirectX.Direct3D.ClearFlags.Target,
ColorToInt(Colors.DarkGray), 1.0f, 0);
Render2dCircle(circleCenterX, circleCenterY, circleRadius,
Colors.Red, mdxHost.D3dDevice); Render2dCircle(circleCenterX, circleCenterY, circleRadius - 3,
Colors.DarkRed, mdxHost.D3dDevice); Render2dCircle(circleCenterX, circleCenterY, 5, Colors.Salmon, mdxHost.D3dDevice);
Render2dText(string.Format("Circle center: ({0},{1})", circleCenterX, circleCenterY),
(int)(circleCenterX - circleRadius), (int)(circleCenterY + circleRadius + 10),
36f, Colors.White, mdxHost.D3dDevice);
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:
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:
private void mdxHost_D3dSurfaceMouseLeave(object sender, D3dSurfaceMouseEventArgs e)
{
surfaceMousePosition.Text = "Out of surface";
}
The result can be shown like the following:
History
- 23 March 2012 - Initial version.
- Current - Addition of an option (the default option) to present the MDX scene using a
D3DImage
.