The other new technology that I am very interested in is DirectX 11. Since both the hardware and the software are very new, there are precious few tutorials or books out there on how to get started. While I am certainly no authority on either F# or DirectX, I hope my learning experience will help give others a head start.
While XNA already provides an excellent way for .NET programmers to play with game programming and computer graphics, it only supports DirectX 9 and Shader Model 3. Compute Shaders in DirectX 11 allows us to write more general purpose code on the GPU. Things like game physics and AI can now be calculated on the GPU without having to jump through all kinds of hoops.
In the absence of XNA there are two options to create DirectX applications for .NET.
- Windows API Code Pack
- SlimDX
I have chosen the latter since it provides access to several other APIs like DirectInput in addition to the graphics wrappers.
I hope to create a series of tutorials over the next couple of weeks. The first will focus on how to get up and running with DirectX 11 and highlight a couple of cool F# features along the way. Today we will create a basic render window that renders a blue screen. In order for this example to work, you will need a DirectX 11 graphics card. At the moment the ATI Radeon 5800 series are the only cards that support this.
Setting up your project
-Create a new F# Application. In Visual Studio 2010 Beta 2 this will be a console application by default. Change the Ouput type to 'Windows Application' under Properties->Application tab.
- Download SlimDX and reference the assembly in your F# project.
- Add references to System.Windows.Forms and System.Drawing
- Set the target framework to .NET 4.0 (Not the client framework)
The code
To prevent having to write the fully qualified class names we will use a couple of import declarations. In F# we use the 'open' keyword.
open System
open System.Windows.Forms
open System.Drawing
open SlimDX
open SlimDX.Direct3D11
open SlimDX.DXGI
The first thing we will need is a device to access the graphics hardware. We will also need a swap chain which will allow us to present the front buffer after drawing a frame.
let deviceAndSwapChain windowHandle width height =
let modeDescription = new ModeDescription(
Width = width,
Height = height,
Format = Format.R8G8B8A8_UNorm,
RefreshRate = new Rational(60,1))
let swapChainDescription = new SwapChainDescription(
BufferCount = 1,
ModeDescription = modeDescription,
Usage = Usage.RenderTargetOutput,
OutputHandle = windowHandle,
SampleDescription = new SampleDescription(Count = 1),
IsWindowed = true,
SwapEffect = SwapEffect.Discard)
let _, device, swapChain = Device.CreateWithSwapChain(
null,
DriverType.Hardware,
DeviceCreationFlags.None,
swapChainDescription )
device, swapChain
There are a couple of interesting things about this first function.
- We defined a function that takes three parameters: a window handle, the screen width and the screen height. Yet we did not specify any types. One of F#'s strengths is that even though it is a statically typed language, you rarely need to specify any types. Most of the time they can be inferred by the compiler. This removes a lot of the clutter that you are forced to write in other languages like C# or C++ without sacrificing type safety.
- The arguments to a function can be separated by spaces. This is by personal choice. It could have been written as:
let deviceAndSwapChain(windowHandle, width, height)
The first form also allows you to do a funky thing called currying, which I will not get into now.
- In F# there are no need for curly braces. Statement blocks are implied by the tabbing.
- Note how we can initialize the ModeDescription and SwapChainDescription structures by specifying the values in what looks like the constructor, even though there is no such constructor. This is some F# syntactical sugar to avoid the much clumsier classical way of initializing a struct:
let deviceAndSwapChain windowHandle width height =
let modeDescription = new ModeDescription()
modeDescription.Width <- width
modeDescription.Height <- height
modeDescription.Format <- Format.R8G8B8A8_UNorm
modeDescription.RefreshRate <- new Rational(60,1)
- There is no 'return' statement at the end of the function definition. It is implicit.
- We can return multiple values. These are wrapped as a tuple.
Next up is our main function. We have to apply the EntryPoint attribute to indicate that this is where the application should start. We first create our form in order to get a valid window handle. We then use this handle with the screen dimensions to create our device and swap chain.
[<STAThread>]
[<EntryPoint>]
let main(args) =
let width, height = 800, 600
let form = new Form(Text = "Flow Edit", Width = width, Height = height)
let device, swapChain = deviceAndSwapChain form.Handle width height
We need a RenderTargetView in order to gain access the back buffer. This will be cleared at the start of each frame during the the callback to 'paint'. We register to the Paint event by adding our own handler that clears the back buffer and flips it using the swap chain. Note that since we do not use the PaintEventArgs in the callback, we simply name the argument with an underscore, which happens to be a valid identifier in F#. The struct initialization syntax again comes in handy. This time there is actually a constructor available that takes 4 float arguments. When inspecting the code however, new Color4(1.0f,0.0f,0.0f,1.0f) would be a bit ambiguous. Is this ARGB (blue) or RGBA (red)? By providing named parameters we avoid any confusion.
let renderTargetView =
use renderTarget = Resource.FromSwapChain(swapChain, 0)
new RenderTargetView(device, renderTarget)
let paint _ =
do device.ImmediateContext.ClearRenderTargetView(
renderTargetView,
new Color4(Alpha = 1.0f, Red = 0.0f, Green = 0.0f, Blue = 1.0f))
do swapChain.Present(0, PresentFlags.None) |> ignore
form.Paint.Add(paint)
When we run our form, lo and behold, we have a window rendering a glorious blue sky. ;-)
do Application.Run(form)
When the form is closed, we need to dispose of any SlimDX objects. For this simple example, I just rely on the ObjectTable to make sure that all objects are disposed.
for item in ObjectTable.Objects do
item.Dispose()
0
Below is the full source code listing. You can find the Visual Studio 2010 solution at Google Code
open System
open System.Windows.Forms
open System.Drawing
open SlimDX
open SlimDX.Direct3D11
open SlimDX.DXGI
let deviceAndSwapChain windowHandle width height =
let modeDescription = new ModeDescription(
Width = width,
Height = height,
Format = Format.R8G8B8A8_UNorm,
RefreshRate = new Rational(60,1))
let swapChainDescription = new SwapChainDescription(
BufferCount = 1,
ModeDescription = modeDescription,
Usage = Usage.RenderTargetOutput,
OutputHandle = windowHandle,
SampleDescription = new SampleDescription(Count = 1),
IsWindowed = true,
SwapEffect = SwapEffect.Discard)
let _, device, swapChain = Device.CreateWithSwapChain(
null,
DriverType.Hardware,
DeviceCreationFlags.None,
swapChainDescription )
device, swapChain
//-----------------------------------------------------------------------------------
[<STAThread>]
[<EntryPoint>]
let main(args) =
let width, height = 400, 300
let form = new Form(Text = "Blue skies", Width = width, Height = height)
let device, swapChain = deviceAndSwapChain form.Handle width height
let renderTargetView =
use renderTarget = Resource.FromSwapChain(swapChain, 0)
new RenderTargetView(device, renderTarget)
let paint _ =
do device.ImmediateContext.ClearRenderTargetView(
renderTargetView,
new Color4(Alpha = 1.0f, Red = 0.0f, Green = 0.0f, Blue = 1.0f))
do swapChain.Present(0, PresentFlags.None) |> ignore
form.Paint.Add(paint)
do Application.Run(form)
for item in ObjectTable.Objects do
item.Dispose()
0
Hello! I'm trying run this sample, but take
ReplyDeleteException: "Method 'SlimDX.Direct3D11.Resource.FromPointerReflectionThunk' not found."
at line: use renderTarget = Resource.FromSwapChain(swapChain, 0). Need add <Texture2D>: use renderTarget = Resource.FromSwapChain<Texture2D>(swapChain, 0)
P.S.: Sorry for my bad English:)