Minimal DirectX demo
With the advent of .NET, Managed DirectX makes graphical programming easy under Windows. This program is a minimal DirectX demo with several interesting features:
- Lighting
- Perspective
- Mouse control
- Mesh rendering
Despite having all of these features, the entire program requires only 100 lines of source code to render a 3D teapot that can be rotated with the mouse:
This example program is freely available in two forms:
- Standalone Windows executable (requires Microsoft .NET 2 and DirectX)
- Source code: teapot.fs
- Visual Studio 2005 project
The program has several interesting aspects:
GUI
This is a Windows application that uses WinForms to create a window containing a DirectX device.
The WinForms-related code is written in an object oriented style, forming a Viewer
class:
type Viewer = class inherit Form ... end
This class encapsulates the creation and handling of a window containing a DirectX device. The constructor for this class accepts the title of the window as a string and a rendering function:
new(title, render) as form = {device=null; render=render; world=Matrix.Identity; drag=None} then form.SetStyle(Enum.combine [ControlStyles.AllPaintingInWmPaint; ControlStyles.Opaque], true); form.TextNote that the ability to pass the rendering function as an argument to the constructor is functional programming.
Mouse control
Dragging is achieved by storing a mutable value in the
Viewer
object:val mutable drag : (Matrix * int * int) optionWhen the scene is not being dragged,
drag
isNone
. During dragging,drag
isSome(m, x, y)
wherem
is the original world transformation matrix andx, y
is the window coordinate where the drag started.Inside the
Viewer
class, theOnMouseDown
event is used to setdrag
:override form.OnMouseDown e = form.dragand the
OnMouseUp
event is used to resetdrag
:override form.OnMouseUp e = form.dragThe
OnMouseMove
event usesdrag
to update the world transformation matrix during a drag and then force redraw:override form.OnMouseMove e = match form.drag with | Some(world, x, y) -> let scale = 5.f / float32(min form.device.Viewport.Width form.device.Viewport.Height) in form.world ()This is a very succinct way to handle the mouse control of a scene. Note the use of variant types (
option
) and pattern matching.Rendering
The
OnPaint
event of the window calls boiler-plate DirectX functions and uses therender
function that the object was instantiated with to perform the actual rendering:override form.OnPaint _ = if form.device = null then form.make_device(); try form.device.BeginScene(); form.device.Clear(Enum.combine [ClearFlags.Target; ClearFlags.ZBuffer], Color.Black, 1.f, 0); form.device.Transform.World form.make_device()Note that this function is careful to replace the DirectX device if there is an error. This handles DirectX device loss and reset, although there are probably more elegant ways to do so. For example, if the
render
function threw one of our own exceptions, the device might not need replacing.The
render
function, responsible for rendering the scene, is defined outside theViewer
class. The render function sets a light:device.RenderState.SpecularEnableinitialises the transformation matrices (representing perspective projection and the camera position and orientation relative to the scene):
let aspect = float32 device.Viewport.Width / float32 device.Viewport.Height in device.Transform.Projectionsets materials propertes ready to render a red teapot with a white specular highlight:
let mutable material = new Material() in material.Diffuseand finally renders the built-in teapot mesh:
Idioms.using (Mesh.Teapot(device)) (fun teapot -> teapot.DrawSubset(0))Note the use of
Idioms.using
to ensure that the teapot mesh is deallocated immediately. If the mesh is create and then left for garbage collection every frame, many meshes accumulate and stall the program when the garbage collector kicks in. In this case, a better practice is to store the mesh along with the device and invalidate it when the device is reset.Despite the sophistication of this application, the entire program is tiny thanks to the expressiveness of the F# programming language.