Thursday 17 April 2014

Geometry and Functions

After finishing my projects, I finally have time to work on some way long overdue features.

I'm doing a lot of work on my user interface revamp, and also doing a lot of code cleanup since now Windows Phone gives us access (also long overdue) to all nice features like P/Invoke, Direct2d....

This means after a bit of tweaking on SharpDX code base, windows phone is now AnyCpu, which is really great, having x86, x64, ARM and lot of targets to maintain is not pleasing at all.

Also I decided to move on and remove the old DirectX11/DirectX11.1 targets on my toolbox. No more Windows 7 support, I prefer to focus on having a better build for Desktop / RT / Phone instead.

Now after all this I thought it was time to work on another (well overdue as well) feature.

I've always been interested into processing geometry in compute shaders instead of using StreamOut.

This gives some pretty useful advantages:

  • Write in place : RWStructuredBuffer rocks, no need for ping/pong
  • Writing Indexed geometry using Geometry Shader is doable, but rather painful.
  • Much easier to do more advanced post processing
  • Access to indirect Draw, which means it's much easier to instance generated geometry, StreamOut does not give us access to internal counter, except using a query, eg: not ideal.
Here are my structures:

Code Snippet
  1. private DX11StructuredBuffer positionbuffer;
  2. private DX11StructuredBuffer normalsbuffer;
  3. private DX11StructuredBuffer uvbuffer;
  4.  
  5. private DX11StructuredBuffer appendindexbuffer;
  6.  
  7. private DX11InstancedIndexedDrawer drawer;
  8. private DispatchIndirectBuffer vertexIndirectDispatch;
  9. private DispatchIndirectBuffer indexIndirectDispacth;

Pretty simple, with two things to note:
  • Position buffer is created with the counter flag
  • Index Buffer is created with the append flag
Now there are a few bits to consider:
  • Structured Buffer is not bindable as Vertex Buffer: This is more or less a non issue in my case, since most of my shaders already fetch data from StructuredBuffers using SV_VertexID
  • StructuredBuffer is not bindable as Index Buffer. This is more annoying, since you need to bind it to the pipeline, no way to fetch. This is quite easy to sort still, as you can just create a standard index buffer (which allow raw view and give it UAV access), then just use a compute shader to copy indices.
So with this setup we are more or less set, you can process per vertex using VertexIndirectDispatcher, process per face using IndexIndirectDispatcher, and since you have access to counter/append, you can also easily emit/remove Geometry.

Now one main issue with this setup, you need to feed your buffers with some initial geometry. 
You have a very simple way to do this in DirectX11.1 , but the feature is only supported on ATI.

So instead, let's replace my old monolithic geometry builders by something more flexible

For now my geometry builders look like this:

Code Snippet
  1. public DX11IndexedGeometry QuadNormals(Quad settings)
  2. {
  3.     DX11IndexedGeometry geom = new DX11IndexedGeometry(device);
  4.     geom.Tag = settings;
  5.     geom.PrimitiveType = settings.PrimitiveType;
  6.  
  7.     List<Pos4Norm3Tex2Vertex> vertices = new List<Pos4Norm3Tex2Vertex>();
  8.     List<int> indices = new List<int>();
  9.  
  10.     //Build your array
  11.  
  12.     geom.VertexBuffer = DX11VertexBuffer.CreateImmutable(device, vertices.ToArray());
  13.     geom.IndexBuffer = DX11IndexBuffer.CreateImmutable(device, indices.ToArray());
  14.     geom.InputLayout = Pos4Norm3Tex2Vertex.Layout;
  15.     geom.VertexBuffer.InputLayout = geom.InputLayout;
  16.     geom.Topology = PrimitiveTopology.TriangleList;
  17.     geom.HasBoundingBox = true;
  18.     return geom;
  19. }

This has a bit too many things to do:

  • Read settings
  • Build Vertices/Indices list
  • Create Geometry resource

The big problem is that all is in one function, so let's improve and decouple this a little bit

First geometry builders do some pretty simple things, append vertices and append faces, so let's change the construction code like this.


Code Snippet
  1. public class SegmentBuilder : IGeometryBuilder<Segment>
  2. {
  3.     public PrimitiveInfo GetPrimitiveInfo(Segment settings)
  4.     {
  5.         float phase = settings.Phase;
  6.         float cycles = settings.Cycles;
  7.         float inner = settings.InnerRadius;
  8.         int res = settings.Resolution;
  9.         bool flat = settings.Flat;
  10.  
  11.         int vcount = res * 2;
  12.         int icount = (res - 1) * 6;
  13.  
  14.         return new PrimitiveInfo(vcount, icount);
  15.     }
  16.  
  17.     public void Construct(Segment settings, Action<Vector3, Vector3, Vector2> appendVertex, Action<Int3> appendIndex)
  18.     {
  19.  
  20.     }
  21. }

Now we have one builder class responsible to build geometry, but it has no idea of which data structure we use anymore, which is great, since now it's very easy to:

  • Use different data back end (DataStream, List...)
  • Pack data the way we want
  • Completely bypass something (if resolution parameter does not change for example, we can set appendIndex function to null, so we completely ignore building faces).

Here is a simple example to append data into lists:

Code Snippet
  1. public class ListGeometryAppender
  2. {
  3.     private List<Pos4Norm3Tex2Vertex> vertices = new List<Pos4Norm3Tex2Vertex>();
  4.     private List<int> indices = new List<int>();
  5.  
  6.     public List<Pos4Norm3Tex2Vertex> Vertices { get { return this.vertices; } }
  7.     public List<int> Indices { get { return this.indices; } }
  8.  
  9.     public void AppendVertex(Vector3 position, Vector3 normal, Vector2 uv)
  10.     {
  11.         Pos4Norm3Tex2Vertex v = new Pos4Norm3Tex2Vertex()
  12.         {
  13.             Position = new Vector4(position.X, position.Y, position.Z, 1.0f),
  14.             Normals = normal,
  15.             TexCoords = uv
  16.         };
  17.         this.vertices.Add(v);
  18.     }
  19.  
  20.     public void AppendIndex(Int3 index)
  21.     {
  22.         this.indices.Add(index.X);
  23.         this.indices.Add(index.Y);
  24.         this.indices.Add(index.Z);
  25.     }
  26.  
  27.     public void TransformVertices(Func<Pos4Norm3Tex2Vertex,Pos4Norm3Tex2Vertex> transformFunction)
  28.     {
  29.         for (int i = 0; i < this.vertices.Count; i++)
  30.         {
  31.             Pos4Norm3Tex2Vertex v = vertices[i];
  32.             vertices[i] = transformFunction(v);
  33.         }
  34.     }
  35. }


Now to construct our geometry, we can simply do:

Code Snippet
  1. public partial class PrimitivesManager
  2. {
  3.     public DX11IndexedGeometry Cylinder(Cylinder settings)
  4.     {
  5.         CylinderBuilder builder = new CylinderBuilder();
  6.         ListGeometryAppender appender = new ListGeometryAppender();
  7.         PrimitiveInfo info = builder.GetPrimitiveInfo(settings);
  8.         builder.Construct(settings, appender.AppendVertex, appender.AppendIndex);
  9.         return FromAppender(settings, appender, info);
  10.     }
  11.  
  12.     private DX11IndexedGeometry FromAppender(AbstractPrimitiveDescriptor descriptor, ListGeometryAppender appender, PrimitiveInfo info)
  13.     {
  14.         DX11IndexedGeometry geom = new DX11IndexedGeometry(device);
  15.         geom.Tag = descriptor;
  16.         geom.PrimitiveType = descriptor.PrimitiveType;
  17.         geom.VertexBuffer = DX11VertexBuffer.CreateImmutable(device, appender.Vertices.ToArray());
  18.         geom.IndexBuffer = DX11IndexBuffer.CreateImmutable(device, appender.Indices.ToArray());
  19.         geom.InputLayout = Pos4Norm3Tex2Vertex.Layout;
  20.         geom.Topology = PrimitiveTopology.TriangleList;
  21.         geom.HasBoundingBox = info.IsBoundingBoxKnown;
  22.         geom.BoundingBox = info.BoundingBox;
  23.         return geom;
  24.     }
  25. }

What is great by passing functions instead of interfaces, it's much easier to composite actions, for example, we can easily compute bounding box while we create our List.

Code Snippet
  1. SegmentBuilder builder = new SegmentBuilder();
  2. ListGeometryAppender appender = new ListGeometryAppender();
  3. PrimitiveInfo info = builder.GetPrimitiveInfo(settings);
  4.  
  5. Vector3 max = new Vector3(float.MinValue, float.MinValue, float.MinValue);
  6. Vector3 min = new Vector3(float.MaxValue, float.MaxValue, float.MaxValue);
  7.  
  8. builder.Construct(settings, (v, n, u) =>
  9.     { appender.AppendVertex(v, n, u); min = Vector3.Min(min, v); max = Vector3.Max(max, v); }, appender.AppendIndex);

We can also attach several Appenders, like:

Code Snippet
  1. SegmentBuilder builder = new SegmentBuilder();
  2.  
  3. ListGeometryAppender appender = new ListGeometryAppender();
  4. MultiListGeometryAppender multilist = new MultiListGeometryAppender();
  5. builder.Construct(settings, (v, n, u) =>
  6.     {
  7.         appender.AppendVertex(v, n, u);
  8.         multilist.AppendVertex(v, n, u);
  9.     }
  10.     ,appender.AppendIndex);

And you noticed we can optimize by only filling one index buffer ;)

To build StructuredBuffers instead of geometry, here we are:

Code Snippet
  1. SegmentBuilder builder = new SegmentBuilder();
  2. MultiListGeometryAppender appender = new MultiListGeometryAppender();
  3.  
  4. builder.Construct(settings, appender.AppendVertex, appender.AppendIndex);
  5.  
  6. DX11StructuredBuffer intialposition = DX11StructuredBuffer.CreateImmutable<Vector3>(device, appender.Positions.ToArray());
  7. DX11StructuredBuffer initialindices = DX11StructuredBuffer.CreateImmutable<Int3>(device, appender.Indices.ToArray());

Also, we can write to a dynamic buffer directly:

Code Snippet
  1. SegmentBuilder builder = new SegmentBuilder();
  2. PrimitiveInfo info = builder.GetPrimitiveInfo(settings);
  3. DX11StructuredBuffer dynamicposition = DX11StructuredBuffer.CreateDynamic<Vector3>(device, info.VerticesCount);
  4. DataStream ds = dynamicposition.MapForWrite(context);
  5. builder.Construct(settings, (p,n,u) => ds.Write(p) , null);
  6. dynamicposition.Unmap(context);


Here we are, next post I'll show a few examples of what we can now do with compute geometry (and some new idea for particle system ;)