Monday, 8 June 2015

Particles Revamp Part2 : Regions

So in previous post I explained how to move the count management to be managed purely in GPU.

That's of course the initial prerequisite to some more interesting concepts.

So now one common issue is that I need some emitters not to overlap which each other, some behaviours to only apply to some emitters, and some to apply globally.

So that means I need to define locations where to emit, and also restrict some behaviours/colliders to those locations.

I could of could manage some offset tables in all my compute shaders, but that means you need to change code in all of them to eventually take that into account.

So for example, for a simple gravity:

Code Snippet
  1. [numthreads(64, 1, 1)]
  2. void CS_Accumulate( uint3 i : SV_DispatchThreadID )
  3. {
  4.     if (i.x > EmitCount) { return; }
  5.     RWForceBuffer[i.x] += g;
  6. }

Is now replaced by:

Code Snippet
  1. cbuffer cbParticleRangeData : register(b5)
  2. {
  3.     uint startOffset;
  4. };
  6. [numthreads(64, 1, 1)]
  7. void CS_Accumulate( uint3 i : SV_DispatchThreadID )
  8. {
  9.     if (i.x > EmitCount) { return; }
  10.     RWForceBuffer[i.x + startOffset] += g;
  11. }

This is not a big change, but it needs to be done in every behaviour, so that takes a bit of time.

Also for emitters the logic is a bit more complex, since you also need to enforce the fact that you don't span regions (eg: don't write a particle in another region location).

So maybe there must be a way to be able to do that in a more elegant way....

Of course there is!

So first, all my particle data is stored in a ParticleDataStore, which contains all buffers, SRV and UAVs. So any particle effector can request to attach any attribute for read or write, depending on use case.

Also every effector receive a dedicated context which also restricts what the effectors have access to (for example, a collider can't attach attributes for writing, except the collision buffer).

So now the idea is to create a new data store, which operates on the same global buffers as the main particle system, but are only allowed in a subset of that buffer.

This is easily possible in DirectX11, which allows to create views for only a part of a buffer.

When we create a buffer (let's say 1024 elements), we also can create default views, which are done as this :

Code Snippet
  1. this.device = device;
  2. this.ElementCount = elementcount;
  3. this.Stride = stride;
  4. this.Buffer = new SharpDX.Direct3D11.Buffer(device.Device, desc);
  5. this.ShaderView = new ShaderResourceView(device.Device, this.Buffer);
  6. this.BufferMode = buffermode;
  8. UnorderedAccessViewDescription uavd = new UnorderedAccessViewDescription()
  9. {
  10.     Format = SharpDX.DXGI.Format.Unknown,
  11.     Dimension = UnorderedAccessViewDimension.Buffer,
  12.     Buffer = new UnorderedAccessViewDescription.BufferResource()
  13.     {
  14.         ElementCount = this.ElementCount,
  15.         Flags = (UnorderedAccessViewBufferFlags)buffermode
  16.     }
  17. };
  19. this.UnorderedView = new UnorderedAccessView(device.Device, this.Buffer,uavd);

So in case of shader view, I don't pass a descriptor (which means the view is for the whole buffer).

In case of UAV I pass a descriptor (since my view can also have an append/counter flag).

But maybe an acute reader already noticed, there is an ElementCount parameter in the UAC descriptor, so maybe there's also a FirstElement parameter?

Of course there is.

So in order to operate on a subset of our buffer, we can instead do:

Code Snippet
  1. public DX11StructuredBufferRegion(DxDevice device, DX11StructuredBuffer parentBuffer, int StartOffset, int ElementCount)
  2. {
  3.     this.Buffer = parentBuffer.Buffer;
  4.     this.ElementCount = ElementCount;
  5.     this.Stride = parentBuffer.Stride;
  7.     UnorderedAccessViewDescription uavd = new UnorderedAccessViewDescription()
  8.     {
  9.         Format = SharpDX.DXGI.Format.Unknown,
  10.         Dimension = UnorderedAccessViewDimension.Buffer,
  11.         Buffer = new UnorderedAccessViewDescription.BufferResource()
  12.         {
  13.             ElementCount = this.ElementCount,
  14.             Flags = UnorderedAccessViewBufferFlags.None,
  15.             FirstElement = StartOffset
  16.         }
  17.     };
  19.     ShaderResourceViewDescription srvd = new ShaderResourceViewDescription()
  20.     {
  21.         Format = SharpDX.DXGI.Format.Unknown,
  22.         Dimension = ShaderResourceViewDimension.Buffer,
  23.         BufferEx = new ShaderResourceViewDescription.ExtendedBufferResource()
  24.         {
  25.             ElementCount = this.ElementCount,
  26.             FirstElement = StartOffset
  27.         }
  28.     };
  30.     this.ShaderView = new ShaderResourceView(device, parentBuffer.Buffer, srvd);
  31.     this.UnorderedView = new UnorderedAccessView(device, parentBuffer.Buffer, uavd);
  32. }

We keep the same buffer, but in that case we specify which locations our view are operating on.

So for example, we can say:
This view operates from element 512, and has 200 elements.

The pretty neat thing is, now in our shader, when we say for example

RWForceBuffer[0] = force;

We are actually operating on element 512!

Pretty neat no?

So instead of adding all this crazy offset logic, we just create a new datastore (which operates on he same buffers), but that creates restricted views, and pass that to our effectors, which don't even know they operate on a subset of the data (and they should not even know it).

So next part was simply to add a region handler (which can accept his own effectors), and a new Particle system which can also accept global effectors (now particle system operation with regions can't accept emitters anymore, we build regions to avoid overlap, so emitters should only be restricted).

Particle system now, instead of dispatching globals, now has to apply locals pre region, then globals, little bit of extra work but nothing too hard.

So now here is a small example (super basic rendering but gives the idea):

Here random emitter and sphere emitter both operate on a separate region (you can stil stack several emitters in the same region of course).

Each one has it's own color palette, sphere emitter has damping, but random emitter doesn't.

Random emitter has one extra collider.

Gravity, and the 2 colliders are linked to the particle system, so they apply to all regions.

As much as it is a simple example, I'm pretty sure it's easy to see the potential and the flexibility it provides, and the great thing is that I did not have to change a single line of code in my effector/behaviour/colliders shader code (life is good at times).

Here we are for part 2, for the next one I'll explain another cool feature (codename: Selectors)

No comments:

Post a Comment