Project page

Grass rendering in C# Direct3D 11 with SlimDX




Project sources is available here :
https://github.com/kobr4/direct3D-projects/tree/master/MyHelloWorldSlimDxWithMMV

This little projet is about rendering animated grass.
And because I'd like to learn something new, for this projet, I will use Direct3D the simplest way, using C# and native wrapper that is SlimDX, time to get back to Visual Studio environnment.

Let's face it, although I praise the speed of native language like C++, for a hobbist programmer, C# nad Java offer a much higher productivity due to the well documented and feature-rich framework, ease of debugging, and of course garbage collection.

Grass rendering is a much discussed subject, some paper :
http://http.developer.nvidia.com/GPUGems/gpugems_ch07.html
http://outerra.blogspot.fr/2012/05/procedural-grass-rendering.html

Setup an environnement
To get started, I followed this tutorial that gets to render on triangle in the middle of the screen.

http://slimdx.org/tutorials/SimpleTriangle.php

Great but it's only a single "main" program, so I "objectized" it and I've added the matrix transformations mechanism ( model / view / project).

 
public class Renderer
{
Device device;
SwapChain swapChain;
ShaderSignature inputSignature;
VertexShader vertexShader;
PixelShader pixelShader;
RenderTargetView renderTarget;
InputLayout layout;
InputElement[] elements;
DeviceContext context;
RenderForm form;
List<RenderableInterface> renderableList = new List<RenderableInterface>();
SlimDX.Direct3D11.Buffer inputBuffer;
 
 
// Matrices
Matrix worldMatrix = Matrix.RotationY(0.5f);
Matrix viewMatrix = Matrix.Translation(0, 0, 5.0f);
Matrix finalMatrix = Matrix.Identity;
const float fov = 0.8f;
Matrix projectionMatrix;
 
Camera camera;
public void init()
{
form = new RenderForm("My HelloWorld SlimDX with Matrix Model View app");
var description = new SwapChainDescription()
{
BufferCount = 1,
Usage = Usage.RenderTargetOutput,
OutputHandle = form.Handle,
IsWindowed = true,
ModeDescription = new ModeDescription(0, 0, new Rational(60, 1), Format.R8G8B8A8_UNorm),
SampleDescription = new SampleDescription(1, 0),
Flags = SwapChainFlags.AllowModeSwitch,
SwapEffect = SwapEffect.Discard
};
 
 
Device.CreateWithSwapChain(DriverType.Hardware, DeviceCreationFlags.None, description, out device, out swapChain);
 
// create a view of our render target, which is the backbuffer of the swap chain we just created
 
using (var resource = Resource.FromSwapChain<Texture2D>(swapChain, 0))
renderTarget = new RenderTargetView(device, resource);
 
// setting a viewport is required if you want to actually see anything
context = device.ImmediateContext;
var viewport = new Viewport(0.0f, 0.0f, form.ClientSize.Width, form.ClientSize.Height);
 
this.projectionMatrix = Matrix.PerspectiveFovLH(fov, form.ClientSize.Width / (float)form.ClientSize.Height, 0.1f, 1000.0f);
 
context.OutputMerger.SetTargets(renderTarget);
context.Rasterizer.SetViewports(viewport);
 
// load and compile the vertex shader
using (ShaderBytecode bytecode = ShaderBytecode.CompileFromFile("triangle.fx", "VShader", "vs_4_0", ShaderFlags.None, EffectFlags.None))
{
inputSignature = ShaderSignature.GetInputSignature(bytecode);
 
vertexShader = new VertexShader(device, bytecode);
}
 
 
// load and compile the pixel shader
using (ShaderBytecode bytecode = ShaderBytecode.CompileFromFile("triangle.fx", "PShader", "ps_4_0", ShaderFlags.None, EffectFlags.None))
pixelShader = new PixelShader(device, bytecode);
 
elements = new[] { new InputElement("POSITION", 0, Format.R32G32B32_Float, 0) };
layout = new InputLayout(device, inputSignature, elements);
 
 
 
// prevent DXGI handling of alt+enter, which doesn't work properly with Winforms
using (var factory = swapChain.GetParent<Factory>())
factory.SetWindowAssociation(form.Handle, WindowAssociationFlags.IgnoreAltEnter);
 
// handle alt+enter ourselves
form.KeyDown += (o, e) =>
{
if (e.Alt && e.KeyCode == System.Windows.Forms.Keys.Enter)
swapChain.IsFullScreen = !swapChain.IsFullScreen;
};
 
// handle form size changes
form.UserResized += (o, e) =>
{
renderTarget.Dispose();
 
swapChain.ResizeBuffers(2, 0, 0, Format.R8G8B8A8_UNorm, SwapChainFlags.AllowModeSwitch);
using (var resource = Resource.FromSwapChain<Texture2D>(swapChain, 0))
renderTarget = new RenderTargetView(device, resource);
 
context.OutputMerger.SetTargets(renderTarget);
};
 
 
BufferDescription inputBufferDescription = new BufferDescription
{
BindFlags = BindFlags.ConstantBuffer,
CpuAccessFlags = CpuAccessFlags.Write,
OptionFlags = ResourceOptionFlags.None,
SizeInBytes = (16 * 4 +16),
StructureByteStride = sizeof(float),
Usage = ResourceUsage.Dynamic,
};
 
inputBuffer = new SlimDX.Direct3D11.Buffer(device, inputBufferDescription);
 
camera = new Camera();
camera.setPosition(0f, 0f, 5f);
}
 
public void start()
{
MessagePump.Run(form, () =>
{
// clear the render target to a soothing blue
context.ClearRenderTargetView(renderTarget, new Color4(0.5f, 0.5f, 1.0f));
//camera.setRotateX(camera.getRotateX() + 0.001f);
Matrix rotationX = Matrix.RotationX(camera.getRotateX());
Matrix rotationY = Matrix.RotationY(camera.getRotateY());
Matrix rotationZ = Matrix.RotationZ(camera.getRotateZ());
Matrix translation = Matrix.Translation(camera.getPosition());
 
viewMatrix = translation * rotationX * rotationY * rotationZ;
 
foreach (RenderableInterface renderable in renderableList)
{
render();
 
finalMatrix = worldMatrix * viewMatrix * projectionMatrix;
int time = (Environment.TickCount/10) % 360;
float costime = (float)Math.Cos(Math.PI / 180d * (double)time);
//Important when not using the Effect direct3d framework
finalMatrix = Matrix.Transpose(finalMatrix);
DataBox input = device.ImmediateContext.MapSubresource(inputBuffer, MapMode.WriteDiscard, SlimDX.Direct3D11.MapFlags.None);
for (int i = 0; i < 16; i++)
input.Data.Write((float)finalMatrix.ToArray()[i]);
input.Data.Write(costime);
device.ImmediateContext.UnmapSubresource(inputBuffer, 0);
 
context.InputAssembler.SetVertexBuffers(0, renderable.getVertices());
context.Draw(renderable.getTriangleCount(), 0);
 
 
}
// draw the triangle
swapChain.Present(0, PresentFlags.None);
});
}
 
public void addRenderable(RenderableInterface renderable)
{
renderable.initBuffers(device);
this.renderableList.Add(renderable);
}
 
public void render()
{
// configure the Input Assembler portion of the pipeline with the vertex data
context.InputAssembler.InputLayout = layout;
context.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList;
 
// set the shaders
context.VertexShader.Set(vertexShader);
context.PixelShader.Set(pixelShader);
 
context.VertexShader.SetConstantBuffer(inputBuffer, 0);
}
 
 
public void dispose()
{
layout.Dispose();
inputSignature.Dispose();
vertexShader.Dispose();
pixelShader.Dispose();
renderTarget.Dispose();
swapChain.Dispose();
device.Dispose();
}
 
}
 


The triangle class
 
public class MyTriangle : RenderableInterface
{
SlimDX.Direct3D11.VertexBufferBinding bufferBinding;
Buffer vertexBuffer;
DataStream vertices;
private int triangleCount = 3;
public MyTriangle()
{
vertices = new DataStream(12 * 3, true, true);
vertices.Write(new Vector3(0.0f, 0.5f, 0.5f));
vertices.Write(new Vector3(0.5f, -0.5f, 0.5f));
vertices.Write(new Vector3(-0.5f, -0.5f, 0.5f));
vertices.Position = 0;
}
 
public void initBuffers(Device device)
{
vertexBuffer = new Buffer(device, vertices, 12 * 3, ResourceUsage.Default, BindFlags.VertexBuffer, CpuAccessFlags.None, ResourceOptionFlags.None, 0);
bufferBinding = new VertexBufferBinding(vertexBuffer, 12, 0);
}
 
public SlimDX.Direct3D11.VertexBufferBinding getVertices()
{
return bufferBinding;
}
 
public void dispose()
{
vertices.Close();
vertexBuffer.Dispose();
}
 
public int getTriangleCount()
{
return triangleCount;
}
}
 


The vertex shader with final transformation matrix
 
cbuffer globals
{
matrix finalMatrix;
}
 
 
float4 VShader(float4 position : POSITION) : SV_POSITION
{
    float4 pos = mul(float4(position ), finalMatrix);
    return pos;
}
 
float4 PShader(float4 position : SV_POSITION) : SV_Target
{
    return float4(1.0f, 1.0f, 0.0f, 1.0f);
}
 


The finalMatrix has to be passed to the shader in an awkward way that consists in allocating a direct3d buffer, then filling it every frame with datas
 
BufferDescription inputBufferDescription = new BufferDescription
{
BindFlags = BindFlags.ConstantBuffer,
CpuAccessFlags = CpuAccessFlags.Write,
OptionFlags = ResourceOptionFlags.None,
SizeInBytes = (16 * 4 +16),
StructureByteStride = sizeof(float),
Usage = ResourceUsage.Dynamic,
};
 
inputBuffer = new SlimDX.Direct3D11.Buffer(device, inputBufferDescription);
 


Then, every frame, copy the transformation matrix to the buffer.
 
DataBox input = device.ImmediateContext.MapSubresource(inputBuffer, MapMode.WriteDiscard, SlimDX.Direct3D11.MapFlags.None);
for (int i = 0; i < 16; i++)
input.Data.Write((float)finalMatrix.ToArray()[i]);
input.Data.Write(costime);
device.ImmediateContext.UnmapSubresource(inputBuffer, 0);
 


First approach

When trying to mimic real world elements, I start by observing what is a grass patch in real life : it's made of randomly spawned blade of individual leaf from different height and that are moving together due the wind condition.

To start my algorithm :
* each grass blade is composed of quads
* each blade is moving back and forth with a growing amplitude from the base (no movement) to the top (maximum movement)
* grass blade are spawned on the ground in a uniform but random fashion

To make blades move gently back we have the cosinus function, that goes from -1 to 1, with current time as argument, it will oscillate smoothly.
In the shader, to determine whether the position of the vertex on the grass blade and move it accordingly, we use texture coordinates where the y will be the bottom vertex and 1.0 the top vertex.

Here is the result:

65536 grass blades are moved in a smooth fashion.

Way of improvements
* A solid ground should be more convincing !
* Blades are strictly straights and verticals, in real life, blades don't grow straight, generating blade with a little random bend would be bette.
* Blades are only facing one direction so when approaching or looking from another direction one can see that it's fake. I can billboard them but that will still be an obivous fake, generating blades in every direction will add much more complexity and will stress the hardware too much. In NVIDIA's paper, blade are not managed individually but as an object that faces all direction.
* Blades are moving together in the same way, in real life, grass will move depending of wind conditions that are not the same in every area.
* Complexity is the same everywhere, for optimisation purpose, the level of detail should be adjusted depending on viewing distance.

24/09/2012
Added generated heightmap, mouse-look.

Not bad at all !

15/12/2012
Let's add a skydome to this landscape. Why skydome instead of skybox ? because skydome show less artifact and are easier to move around to appear as moving clouds.

Drawing a sphere is easy if you remember your math courses about spherical coordinates :

$x = r * \sin(\theta) * \cos(\phi)$
$y = r * \sin(\theta) * \sin(\phi)$
$z = r * \cos(\theta)$

Iterate for $\phi$ from $0$ to $2\pi$ and $\theta$ from $0$ to $\frac{\pi}{2}$ with and here is the sphere !


Wow, it's windy out there !