D3dHost - MDX 和 WPF 互操作





5.00/5 (7投票s)
本文介绍了如何在 WPF 窗口中渲染可互操作的 MDX (Managed DirectX) 场景。
目录
简介
Windows Presentation Foundation (WPF) 提供了用于渲染 3D 图形的一组元素。这些元素非常适合设计 3D 控件和渲染一些简单的 3D 场景。但如果我们想渲染更复杂的场景,可能需要使用更底层的技术。
要使用非托管代码渲染 DirectX 场景,我们可以使用 D3DImage
类,正如《D3DImage 简介》一文中所解释的那样。但如果我们想完全使用托管代码编写整个应用程序,我们可能需要使用一个支持使用托管代码渲染 DirectX 场景的框架。
要使用托管代码渲染 DirectX 场景,我们有 Managed DirectX (MDX) 框架。使用 MDX,我们可以创建一个 Device
并使用其方法渲染我们的场景。要在 WPF 窗口中渲染 MDX 场景,我们可以 使用 WPF 窗口的句柄创建 MDX 设备,或者 使用托管在 WPF 窗口中的 Windows.Forms
控件创建 MDX 设备,该控件通过 WindowsFormsHost
进行托管。
这些技术可能适用于渲染 MDX 场景的独立区域。但当我们想与其他 WPF 元素交互时,会发现 WPF 的某些效果(例如,不透明度、事务等)无法按预期工作。
本文介绍了如何在 WPF 窗口中将 MDX 场景渲染为可互操作的 WPF 控件。
工作原理
渲染 MDX 场景
创建用于容纳 MDX 设备的控件
为了支持 WPF 和 MDX 之间的互操作性,我们创建了一个 WPF 自定义控件来托管 MDX 场景。
public class D3dHost : Control
{
static D3dHost()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(D3dHost),
new FrameworkPropertyMetadata(typeof(D3dHost)));
}
}
在该控件中,我们添加了一个 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
并使用该 Panel
创建一个 MDX Device
。
#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
创建用于渲染 MDX 场景的区域
要在 WPF 控件上渲染 MDX 场景,我们为渲染 MDX 场景的区域添加了一个 TemplatePart
。
[TemplatePart(Name = "PART_D3dRegion", Type = typeof(Rectangle))]
public class D3dHost : Control
{
}
创建一个包含 Rectangle
的默认样式,该 Rectangle
以 TemplatePart
的名称命名。
<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>
并根据名称查找 Rectangle
。
private Rectangle _d3dRegion;
public override void OnApplyTemplate()
{
_d3dRegion = GetTemplateChild("PART_D3dRegion") as Rectangle;
base.OnApplyTemplate();
}
渲染 MDX 设备的表面
要渲染 MDX 场景,我们获取 MDX 表面的副本,并将其设置为 Rectangle
的 Fill
。我们可以通过以下几种方式实现:
使用
D3DImage
#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
使用内存中的缓冲区
#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
使用磁盘上的文件
#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
由于我们不希望在场景繁重时阻塞 UI,因此我们在另一个线程中更新 3D 区域。
#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);
}
为确保场景在呈现之前完全渲染,我们添加了指示绘图开始和结束的方法。
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();
}
通知 MDX 区域大小的变化
为了通知托管 MDX 场景的区域的大小变化,我们添加了该区域的实际宽度和高度的属性。
#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
并添加了一个 RoutedEvent
,每次 MDX 场景区域的大小发生变化时都会引发该事件。
#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();
}
通知鼠标事件
为了通知在 MDX 场景上发生的鼠标事件,我们为每个鼠标事件创建一个 RoutedEvent
(GotMouseCapture
、LostMouseCapture
、MouseEnter
、MouseLeave
、MouseMove
、MouseDown
、MouseLeftButtonDown
、MouseLeftButtonUp
、MouseRightButtonDown
、MouseRightButtonUp
、MouseUp
、MouseWheel
、PreviewMouseDown
、PreviewMouseLeftButtonDown
、PreviewMouseMove
、PreviewMouseRightButtonDown
、PreviewMouseRightButtonUp
、PreviewMouseUp
和 PreviewMouseWheel
)。例如,这是 MouseMove
事件的 RoutedEvent
:
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); }
}
为了使用 MDX 表面上的鼠标位置引发适当的 RoutedEvent
,我们获取表面的鼠标位置。
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;
}
获取适当的鼠标事件。
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;
}
并在原始鼠标事件的事件处理程序中引发适当的鼠标事件。
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);
}
}
如何使用
环境设置
防止出现“LoaderLock 已检测到”弹出窗口
在某些情况下,调试代码时可能会出现“LoaderLock 已检测到”的弹出窗口。为了停止它,我们可以选择“Debug”菜单下的“Exceptions”选项,然后在“Managed Debugging Assistants”组下取消选中“LoaderLock”项。
设置配置以支持“混合模式程序集”
为了支持在 .NET 4 上使用混合模式程序集,我们可以将应用程序配置的 startup
元素的 useLegacyV2RuntimeActivationPolicy
属性设置为 true
,如下所示:
<configuration>
<startup useLegacyV2RuntimeActivationPolicy="true">
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
</startup>
</configuration>
渲染场景
为了演示 D3dHost
控件在渲染场景中的使用,我们创建了一个显示一些旋转锥体的窗口。
为了比较 MDX 3D 框架和 WPF 3D 框架,我们使用 MDX 和 WPF 渲染相同的场景。
要渲染场景,我们添加一个 Grid
,其中包含一个用于容纳场景的 ContentControl
和一个用于确定显示的锥体数量的 Slider
。
<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>
创建一个用于保存场景数据的类。
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; }
}
根据 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();
}
}
根据 RenderType
初始化窗口(此值通过窗口构造函数的参数设置)。
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";
}
}
并创建用于更新场景和渲染场景的线程。
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);
}
}
}
RenderScene
方法根据 RenderType
调用 MdxRenderScene
方法或 WpfRenderScene
方法。以下是这些方法的实现:
MDX 场景 |
|
WPF 场景 |
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();
} |
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);
}
} |
|
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;
} |
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);
} |
结果可以如下所示:
![]() |
![]() |
与 WPF 元素交互
为了演示使用 D3dHost
控件在 MDX 和 WPF 之间的互操作性,我们创建了一个显示可互操作 MDX 场景的窗口。
在该窗口中,我们添加了一个 D3dHost
控件来显示场景。
<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>
添加一个 Border
以启用某些效果。
<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>
添加一个 Border
来显示表面的鼠标位置。
<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>
并添加背景以显示不透明度效果。
<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>
为了演示鼠标互操作性,我们绘制了一个圆圈和一个显示圆圈中心位置的文本。
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();
}
处理 D3dSurfaceMouseMove
事件,在鼠标光标位于表面上时显示表面的鼠标位置。
private void mdxHost_D3dSurfaceMouseMove(object sender, D3dSurfaceMouseEventArgs e)
{
surfaceMousePosition.Text = string.Format("({0},{1})",
e.D3dSurfaceMousePosition.X, e.D3dSurfaceMousePosition.Y);
}
并处理 D3dSurfaceMouseLeave
事件,在鼠标光标不在表面上时显示“Out of surface”。
private void mdxHost_D3dSurfaceMouseLeave(object sender, D3dSurfaceMouseEventArgs e)
{
surfaceMousePosition.Text = "Out of surface";
}
结果可以如下所示:

历史记录
- 2012 年 3 月 23 日 - 初始版本。
- 当前 - 添加了一个选项(默认选项),使用
D3DImage
来渲染 MDX 场景。