SlimDX y DirectX 11 - Stream Output
Código fuente de la solución (los controles de cada programa figuran en un textbox en el form principal)
Stream Output
Los buffers de salida del Geometry Shader, aparte de ser enviados al Rasterizer, pueden ser obtenidos por la aplicación a través del Stream Output. Al igual que con los render targets del Output Merger, antes de cada renderizado se setean los buffer targets del SO.
Implementación
Los buffers a utilizar como SOtargets deben ser creados con el flag BindFlags.StreamOutput:
positionBuffer = new Buffer(device, initPositions, initPositions.Length * 4 * 3, ResourceUsage.Default, BindFlags.VertexBuffer | BindFlags.StreamOutput, CpuAccessFlags.None, ResourceOptionFlags.None, 4 * 3);
Como para el sistema de partículas voy a hacer ping-pong entre dos buffers para cada característica (posición y velocidad), se deberá especificar al momento de crearlos que se utilizarán como input del Input Assembler (vertex buffers) como así también como SO targets.
Así como existe VertexBufferBinding para asociar a cada buffer con su formato, semántica, stride, etc, existe StreamOutputBinding:
outbinding = new StreamOutputBufferBinding[] { new StreamOutputBufferBinding(vertexbuffer, 0), new StreamOutputBufferBinding(velocitybuffer, 0) };
Si optamos por manejar los shaders usando la clase Effect (como en mi solución), la especificación de cómo se realiza el bindeo recae en el shader:
technique11 GSUpdatePoints { pass P0 { SetVertexShader(CompileShader(vs_5_0, VShaderUpdatePoints())); SetGeometryShader(ConstructGSWithSO(CompileShader(gs_5_0, GShaderUpdatePoints()), "0:POSITION.xyz; 1:VELOCITY.xy;")); SetPixelShader(NULL); } }
Utilizando ConstructGSWithSO, especificamos la semántica (POSITION) asociada a cada buffer del SO (0:) y las componentes a obtener (.xyz).
Si, en cambio, optamos por compilar el GS por separado y asociarlo directamente al contexto, la especificación se realiza en nuestra aplicación:
StreamOutputElement[] SOelements = new StreamOutputElement[] { new StreamOutputElement(0, "POSITION", 0, 0, 3, 0), new StreamOutputElement(0, "VELOCITY", 0, 0, 2, 1) }; int[] SOstrides = new int[] { 12, 8 }; context.GeometryShader = new GeometryShader(device, shaderBytecode, SOelements, SOstrides, 0);
No lo probé por lo que puede que haya puesto mal algún parámetro, pero básicamente lo que se define es lo mismo: index del stream, semántica, index de la semántica, index de la primer componente a obtener, cantidad de componentes a obtener, index del buffer en el stream y stride.
Finalmente, se asocian los buffers al SO con:
contexto.StreamOutput.SetTargets(outbinding);
GSParticlesAndCollisions2D
La idea de este programita es mostrar un sistema de partículas muy básico (tan básico que ni siquiera se crean y destruyen partículas) y cómo se puede usar el GS para actualizar su estado en función del estado anterior. Las partículas se mueven con movimiento rectilíneo (aunque va cambiando de recta :P) uniformemente variado (básicamente, aceleración).
Me basé en D3DBook Dynamic Particle Systems. El autor agrega cosas interesantes como el manejo de distintos tipos de sistemas de partículas en un mismo GS y la emisión y destrucción de las partículas.
El renderizado consta de dos pasadas (dos técnicas de una pasada cada una). En la primera se actualiza el estado del sistema con un GS que se ejecuta por cada punto y retorna a través del Stream Output los buffers de posición y velocidad actualizados. En la segunda, se expande cada punto a quad y se renderiza en pantalla usando un shader prácticamente igual al mostrado en el post anterior.
Figuras colisionables
Plano normalizado, definido por las componentes de su normal (x,y,z) y la distancia desde el mismo hasta el origen en la dirección de su normal (d). Si bien la componente en Z en 2D no tiene sentido, tampoco molesta :P y deja en evidencia que en 3 dimensiones el plano puede ser representado por un float4.
//Bordes de la pantalla con normales hacia adentro efectoUpdate.SetearParametroElement("CollisionObjects", "fPlanes", 0, new Vector4(1f, 0f, 0f, 1f)); //Izq efectoUpdate.SetearParametroElement("CollisionObjects", "fPlanes", 1, new Vector4(-1f, 0f, 0f, 1f)); //Der efectoUpdate.SetearParametroElement("CollisionObjects", "fPlanes", 2, new Vector4(0f, 1f, 0f, 1f)); //Aba efectoUpdate.SetearParametroElement("CollisionObjects", "fPlanes", 3, new Vector4(0f, -1f, 0f, 1f)); //Arr
Para planos no tan simples, se puede utilizar la clase Plane de SlimDX:
Plane p; p = new Plane(new Vector3(ubicacionPlanos, ubicacionPlanos, 0), new Vector3(-1f, -1f, 0)); p.Normalize(); efectoUpdate.SetearParametroElement("CollisionObjects", "fPlanes", 4, new Vector4(p.Normal, p.D));
Box, definida por las componentes de su punto mínimo (x,y) y su punto máximo (x,y). Para 3 dimensiones haría falta utilizar dos float3.
efectoUpdate.SetearParametroElement("CollisionObjects", "fBoxes", index, new Vector4(boxCenter[index].X - boxSemiWidthHeight[index], boxCenter[index].Y - boxSemiWidthHeight[index], boxCenter[index].X + boxSemiWidthHeight[index], boxCenter[index].Y + boxSemiWidthHeight[index]));
Esfera, definida por las componentes de su centro (x,y,z) y su radio (un escalar).
efectoUpdate.SetearParametroElement("CollisionObjects", "fSpheres", index, new Vector4(sphereCenter[index], 0f, sphereRadius[index]));
Colisiones
Plano
Si la distancia signada de la partícula respecto al plano es menor a 0, la misma se encuentra en el lado negativo del plano. Si en el estado anterior estaba en el positivo (>0) o sobre el plano (=0) entonces lo acaba de traspasar y debo manejar el rebote.
Al colisionar, el vector velocidad es reflejado en función de la normal del plano y la posición de la partícula se actualiza como el punto real de intersección entre el segmento recorrido en este frame y el plano. No debería hacer falta actualizar la posición dado que eventualmente la partícula volvería sobre su recorrido, pero lo hice para evitar que se pierdan partículas (vayan muy lejos y tarden en volver) si, por ejemplo, dejo de renderizar un segundo (probar comentando las dos últimas líneas del algoritmo y manteniendo con el mouse la ventana del programa para que deje de renderizar).
for (int i=0; i < fCantPlanes; i++) { distToPlane = dot(fPlanes[i].xyz, Input[0].Pos) + fPlanes[i].w; newDistToPlane = dot(fPlanes[i].xyz, Output.Pos) + fPlanes[i].w; if (newDistToPlane < 0 && distToPlane >= 0) { Input[0].Vel = reflect(Input[0].Vel, fPlanes[i].xy); //Punto de intersección float d = dot(-fPlanes[i].xyz * fPlanes[i].w - Input[0].Pos.xyz, fPlanes[i].xyz) / dot(fPlanes[i].xyz, fPlanes[i].xyz); Output.Pos.xyz = d * fPlanes[i].xyz + Input[0].Pos.xyz; } }
Box
No estoy seguro de si mi implementación de la colisión es la más eficiente, tal vez haya forma de ahorrar alguna que otra condición.
for (int i=0; i < fCantBoxes; i++) { if (Output.Pos.x > fBoxes[i].x && Output.Pos.x < fBoxes[i].z && Output.Pos.y > fBoxes[i].y && Output.Pos.y < fBoxes[i].w) { if (Input[0].Pos.x = fBoxes[i].z) Input[0].Vel = reflect(Input[0].Vel, float2(1,0)); if (Input[0].Pos.y = fBoxes[i].w) Input[0].Vel = reflect(Input[0].Vel, float2(0,1)); } }
En primera instancia, chequeo que la posición actual esté dentro de la caja verificando si se encuentra dentro del rango definido por sus extremos para cada coordenada. Si esto ocurre, verifico si en el estado anterior se encontraba fuera de la caja y respecto a qué coordenada para reflejar la velocidad en función del vector normal del lado que produce la colisión (notar que es indistinto usar normal y -normal dado que ambas definen a la misma superficie de reflexión).
Sphere
La colisión con la esfera no es matemáticamente exacta. La solución correcta resulta ser más complicada que lo que uno esperaría.
for (int i=0; i < fCantSpheres; i++) { float3 casiNormal = Output.Pos - fSpheres[i].xyz; float3 casiNormalPrev = Input[0].Pos - fSpheres[i].xyz; float radiusSquared = fSpheres[i].w * fSpheres[i].w; if (dot(casiNormal, casiNormal) < radiusSquared && dot(casiNormalPrev, casiNormalPrev) >= radiusSquared) { Input[0].Vel = reflect(Input[0].Vel, normalize(casiNormal)); } }
Si la distancia entre la posición actual y el centro de la esfera es menor al radio y la distancia entre la posición anterior y el centro es mayor al radio, la partícula acaba de atravesar la esfera y es reflejada en función del vector Centro->Posición actual. Esto funciona considerablemente bien siempre y cuando la proporción entre distancia recorrida dentro de la esfera y distancia desde el centro al punto actual sea muy baja. Dejaría de funcionar para casos de demasiada velocidad o esferas demasiado pequeñas.
El vector real de reflexión viene dado por el vector Centro->Punto de colisión por lo que habría que obtener a este punto para calcularlo.
Video
Coming up
Estoy entre Dynamic Shader Linkage, Compute Shader o seguir con el GS y las colisiones básicas pero en 3D (aunque supuestamente la idea de actualizar sistemas usando el GS quedó obsoleta con la aparición del CS).










