2025 on Tumblr: Trends That Defined the Year
RMH
Show & Tell

No title available
dirt enthusiast

Kiana Khansmith
Misplaced Lens Cap

No title available

JVL

Janaina Medeiros
AnasAbdin
i don't do bad sauce passes
ojovivo

#extradirty
YOU ARE THE REASON
h

❣ Chile in a Photography ❣
d e v o n

No title available
almost home
seen from India

seen from United States

seen from United States
seen from United States

seen from United States
seen from Greece

seen from Australia
seen from United States
seen from Germany
seen from United States
seen from Germany
seen from United States

seen from South Korea

seen from United States
seen from South Korea

seen from Norway

seen from Australia

seen from Indonesia

seen from India

seen from Malaysia
@akhanubis
Copyright
Tu video puede incluir el siguiente contenido protegido por derechos de autor:
"Balkanel-Dotakni Me usnama", grabación de sonido administrado por:
Rebeat Digital GmbH
¿Qué significa esto?
Tu video sigue estando disponible en todo el mundo. En algunos casos, los anuncios podrán aparecer junto a tu video. Ten en cuenta que el estado del video puede cambiar si se modifican las políticas seleccionadas por los propietarios de contenido.
FAQ de Rebeat:
1. Why do I receive a message like this from YouTube: Dear xxxx, Your video [...] may have content that is owned or licensed by Rebeat Digital GmbH. No action is required on your part; however, if you are interested in learning how this affects your video, please visit the Content ID Matches section of your account for more information. Sincerely, - The YouTube Team Your video apparently contains audio content for which REBEAT Digital owns the digital distribution rights. These rights authorize REBEAT to sell the music on shops like iTunes, Amazon and others. Because REBEAT Digital is a contractual partner of YouTube, the music is also distributed to their service. YouTube scans their video catalogue for the music and sends a message out to those who uploaded a video which includes audio content to which REBEAT Digital owns the distribution rights.
Un grupo músical Balkanel subió su música a Rebeat, una empresa administradora de contenido digital que tiene un convenio con YouTube. YT busca a aquellos videos que utilicen audio cuyo copyright esté gestionado por/subido en Rebeat y les agrega publicidad a los mismos (creo que links para comprar la canción). Lo curioso es que mi video no tiene esa música sino Kecharitomene de Loreena Mckennitt.
Por lo visto (o, mejor dicho, escuchado), la canción Dotakni Me Usnama de Balkanel es en realidad un robo a mano armada de la de Loreena (Kecharitomene y Dotakni Me Usnama). Kecharitomene es instrumental y lo que hizo Balkanel es agregarle una letra encima.
Dado que enviar un mail pidiendo que retiren el reclamo es engorroso (y que si vamos al caso yo no tengo los derechos sobre la canción de Loreena tampoco :P), resubo el video sin música.
Es una verdadera lástima tener que resetear el contador de visitas del video (ya había pasado las 100!).
Nuevo video:
SlimDX y DirectX 11 - Cache de shaders compilados
Dado que la solución estaba tardando casi 5 minutos en compilar por culpa de unos compute shaders que estoy usando para comparar performance, armé una pequeña clase que se encarga de cachear los shaders. Si el código fuente de un shader no fue modificado respecto del utilizado para compilarlo en la ejecución anterior, no se compila y se utiliza el bytecode almacenado.
El código fuente de la solución entera no lo subo porque tengo que ordenar varias partes, pero dejo todo el código de la clase ShaderCache al final del post.
Pseudo-pseudocódigo:
public static Effect ObtenerEffect(string fileName) { if (BuscarShaderEnCacheYActualizar(fileName)) { bytecode = Leer bytecode desde filename + ".cache" } else { bytecode = CompilarBytecode(fileName) Almacenar bytecode en disco en filename + ".cache" } return CrearEfecto(bytecode) } private static bool BuscarShaderEnCacheYActualizar(string fullFilePath) { md5 = CalcularMD5(fileName, fileBaseDir); Buscar fullFilePath en cacheEntries.txt if (No se encontró) { Agregar la entrada "fullFilePath \t md5" return false; //Nuevo shader, habrá que compilar el bytecode } else { Comparar md5 contra MD5 almacenado if (Son iguales) { return true; //Utilizar bytecode cacheado } else { Actualizar el MD5 almacenado con md5 return false; //Shader modificado, habrá que compilar el bytecode } } } private static string CalcularMD5(string fileName, string fileBaseDir) { md5 = GenerarMD5(fileBaseDir + fileName) //Recursivamente concatenar los MD5 de los #include para detectar cambios en estos foreach (#include que haya en el shader) md5 += CalcularMD5(includeFileName, fileBaseDir) return md5; }
CacheEntries
Para detectar si un shader fue modificado, se mantiene en disco una tabla cuyos registros son de la forma (ruta al archivo del shader \t MD5 de su código fuente). Esta tabla no es más que un archivo de texto tabulado, ubicado en Framework\Resources\ShadersCache\cacheEntries.txt.
MD5
El cálculo del MD5 se realiza utilizando el método MD5CryptoServiceProvider.ComputeHash(Stream) y se aplica recursivamente concatenando el hash de un archivo con el hash de sus #include.
private static string ComputeShaderFileMD5(string fileName, string fileBaseDir) { string md5; using (FileStream fs = new FileStream(fileBaseDir + fileName, FileMode.Open)) { md5 = BitConverter.ToString(new MD5CryptoServiceProvider().ComputeHash(fs)); fs.Position = 0; using (TextReader tr = new StreamReader(fs)) { string line = tr.ReadLine(); while (line != null) { if (line.StartsWith("#include \"")) { string includeName = line.Substring("#include \"".Length, line.Length - "#include \"".Length - "\"".Length); //Recursively compute the MD5 of the include file md5 += ComputeShaderFileMD5(includeName, fileBaseDir); } line = tr.ReadLine(); } } } return md5; }
Ejemplo de cacheEntries.txt:
D:\SlimDX\Framework\Framework\Resources\fxVarios.fx 45-43-29-04-00-39-4E-85-CA-3C-4B-97-7F-68-D5-77 D:\SlimDX\Framework\Framework\Resources\CSvsPSGaussianBlur\PSGaussianFilter.fx A9-81-CA-05-14-DD-1F-AE-06-C7-1E-AE-99-6F-1D-2147-4D-90-CD-5B-F6-35-A0-8E-51-50-35-78-71-E3-BD D:\SlimDX\Framework\Framework\Resources\CSvsPSGaussianBlur\CSGaussianFilterF3.fx 33-63-EF-D9-46-34-C3-0C-25-59-C1-FC-5C-3C-E0-CD46-E7-1E-D8-16-76-CA-5F-3E-BE-E8-9F-A7-4F-59-AA47-4D-90-CD-5B-F6-35-A0-8E-51-50-35-78-71-E3-BD
En el ejemplo se observa que el shader PSGaussianFilter.fx hace #include de un archivo X (su MD5 empieza con 47 y termina con BD) y CSGaussianFilterF3.fx de un archivo Y (46-...-AA) que a su vez incluye a X.
Shaderfile.cache
Una vez detectada una modificación en un archivo, se compila el ShaderBytecode y el mismo se graba en el disco en la misma carpeta que el archivo original, con el nombre original seguido de ".cache".
private static Effect CompileEffect(string fileName, string profile, ShaderFlags shaderFlags, EffectFlags effectFlags, ShaderMacro[] macros, Include includeHandler) { Debug.WriteLine("Compiling shader: " + fileName); using (ShaderBytecode shaderCode = ShaderBytecode.CompileFromFile(fileName, profile, shaderFlags, effectFlags, macros, includeHandler)) { using (FileStream fs = new FileStream(fileName + ".cache", FileMode.Create)) { using (BinaryWriter bw = new BinaryWriter(fs)) { bw.Write(shaderCode.Data.ReadRange((int)shaderCode.Data.Length)); } } return new Effect(Recursos.Device, shaderCode); } }
Pequeña limitación
Dado que no se realiza ningún tipo de lectura de macros o directivas al preprocesador (exceptuando el #include), si un archivo X se modifica dentro de un bloque #if #endif (o #ifdef #endif) y un archivo Y incluye a X, entonces Y se recompilará aún cuando no cumpla la condición de #if presente en X y la modificación hecha le sea trasparente.
ShaderCache.cs
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.Cryptography; using SlimDX; using SlimDX.D3DCompiler; using SlimDX.Direct3D11; using Debug = System.Diagnostics.Debug; using Recursos = Framework.Base; namespace Framework { /// <summary> /// A static class used for storing ShaderBytecode and avoiding compiling shaders that weren't modified /// </summary> public static class ShaderCache { /// <summary> /// Looks for the shader's bytecode cache. If found, returns a new Effect created from the bytecode. If not, compiles the Effect, stores it's bytecode and updates the cache entry. /// </summary> /// <param name="fileName">Full path of the shader file</param> /// <param name="profile">Target profile</param> /// <param name="shaderFlags">ShaderFlags (default ShaderFlags.WarningsAreErrors)</param> /// <param name="effectFlags">EffectFlags (default EffectFlags.None)</param> /// <param name="macros">Array of ShaderMacro (default null)</param> /// <param name="includeHandler">An include handler (default null)</param> /// <returns>The compiled Effect</returns> public static Effect CheckCacheAndCompileEffect(string fileName, string profile, ShaderFlags shaderFlags = ShaderFlags.WarningsAreErrors, EffectFlags effectFlags = EffectFlags.None, ShaderMacro[] macros = null, Include includeHandler = null) { if (CheckAndUpdateCache(fileName)) { byte[] byteCode; using (FileStream fs = new FileStream(fileName + ".cache", FileMode.Open)) { using (BinaryReader br = new BinaryReader(fs)) { byteCode = br.ReadBytes((int)fs.Length); } using (DataStream ds = new DataStream(byteCode, true, false)) { using (ShaderBytecode shaderCode = new ShaderBytecode(ds)) { return new Effect(Recursos.Device, shaderCode); } } } } else return CompileEffect(fileName, profile, shaderFlags, effectFlags, macros, includeHandler); } #region Private methods /// <summary> /// Compiles an Effect and writes its bytecode to the hard drive. /// </summary> /// <param name="fileName">Full path of the shader file</param> /// <param name="profile">Target profile</param> /// <param name="shaderFlags">ShaderFlags (default ShaderFlags.WarningsAreErrors)</param> /// <param name="effectFlags">EffectFlags (default EffectFlags.None)</param> /// <param name="macros">Array of ShaderMacro (default null)</param> /// <param name="includeHandler">An include handler (default null)</param> /// <returns>The compiled Effect</returns> private static Effect CompileEffect(string fileName, string profile, ShaderFlags shaderFlags, EffectFlags effectFlags, ShaderMacro[] macros, Include includeHandler) { Debug.WriteLine("Compiling shader: " + fileName); using (ShaderBytecode shaderCode = ShaderBytecode.CompileFromFile(fileName, profile, shaderFlags, effectFlags, macros, includeHandler)) { using (FileStream fs = new FileStream(fileName + ".cache", FileMode.Create)) { using (BinaryWriter bw = new BinaryWriter(fs)) { bw.Write(shaderCode.Data.ReadRange<byte>((int)shaderCode.Data.Length)); } } return new Effect(Recursos.Device, shaderCode); } } /// <summary> /// Generates the MD5 of the shader, compares it against the value stored in the cache entry, and updates the entry if necessary. /// </summary> /// <param name="fileName">Full path of the shader file</param> /// <returns>True if the bytecode stored in the cache is valid.</returns> private static bool CheckAndUpdateCache(string fullFilePath) { string fileBaseDir = fullFilePath.Substring(0, fullFilePath.LastIndexOf('\\') + 1); string fileName = fullFilePath.Substring(fullFilePath.LastIndexOf('\\') + 1); string md5 = ComputeShaderFileMD5(fileName, fileBaseDir); string[] entries; try { entries = File.ReadAllLines(Recursos.ResourcesDir + "ShadersCache\\cacheEntries.txt"); } catch (FileNotFoundException) { File.Create(Recursos.ResourcesDir + "ShadersCache\\cacheEntries.txt").Dispose(); entries = File.ReadAllLines(Recursos.ResourcesDir + "ShadersCache\\cacheEntries.txt"); } int i; for (i = 0; i < entries.Length && !entries[i].Contains(fullFilePath); i++) /*Not an extra semicolon*/; if (i == entries.Length) { Debug.WriteLine("Shader added to cache: " + fullFilePath); List<string> aux = entries.ToList(); aux.Add(fullFilePath + "\t" + md5); entries = aux.ToArray(); } else { string[] split = entries[i].Split(new char[] { '\t' }); if (split[1] == md5) { Debug.WriteLine("Shader found on cache: " + fullFilePath); return true; } else { Debug.WriteLine("Shader updated on cache: " + fullFilePath); entries[i] = fullFilePath + "\t" + md5; } } File.WriteAllLines(Recursos.ResourcesDir + "ShadersCache\\cacheEntries.txt", entries); return false; } /// <summary> /// Computes the MD5 of a shader file concatenated with the MD5 of each of the included files (#include) /// </summary> /// <param name="fileName">Full path of the shader file</param> /// <returns>MD5 as a string</returns> private static string ComputeShaderFileMD5(string fileName, string fileBaseDir) { string md5; using (FileStream fs = new FileStream(fileBaseDir + fileName, FileMode.Open)) { md5 = BitConverter.ToString(new MD5CryptoServiceProvider().ComputeHash(fs)); fs.Position = 0; using (TextReader tr = new StreamReader(fs)) { string line = tr.ReadLine(); while (line != null) { if (line.StartsWith("#include \"")) { string includeName = line.Substring("#include \"".Length, line.Length - "#include \"".Length - "\"".Length); //Recursively compute the MD5 of the include file md5 += ComputeShaderFileMD5(includeName, fileBaseDir); } line = tr.ReadLine(); } } } return md5; } /// <summary> /// Computes the MD5 of a file /// </summary> /// <param name="fileName">Name of the file</param> /// <param name="fileDir">Directory of the file (ending with \\)</param> /// <returns>MD5 of the file</returns> private static string ComputeFileMD5(string fileName, string fileDir) { using (FileStream fs = new FileStream(fileDir + fileName, FileMode.Open)) { return BitConverter.ToString(new MD5CryptoServiceProvider().ComputeHash(fs)); } } #endregion } }
Charla muy interesante sobre realidad virtual y el dispositivo Oculus Rift (http://www.kickstarter.com/projects/1523379957/oculus-rift-step-into-the-game).
Carmack being Carmack:
I think lower level means something different to you than it does to us.
People forget this: when you look back to your fond memories of Doom or Quake, on there it's all smoothed out and nicely antialiased in your memory. But if you go and boot the old computer back up and run the game there, you have this, somebody called them, bathroom tile sized pixels.
Demo de un prototipo viejo (E3 2012): http://www.youtube.com/watch?v=NYa8kirsUfg
Discografía para descargar en http://www.lukhash.com
SlimDX y DirectX 11 - Tessellation (Hull Shader y Domain Shader)
Código fuente y ejecutable de la solución
Tessellation
Subdivisión de un triángulo, un quad o una isolínea en triángulos (, puntos o líneas) más pequeños para generar un modelo de mayor cantidad de primitivas (msdn). Esto permite reducir el ancho de banda necesario al usar como input al pipeline un modelo low-poly, realizar cálculos por patch en lugar de por primitiva final (siempre y cuando se pueda interpolar después), adaptar la calidad del modelo en función de distancia a cámara o screen space, aplicar eficientemente técnicas como displacement mapping, etc.
Para implementarlo por hardware, DirectX 11 incorpora tres etapas más al pipeline: Hull Shader, Tessellation Stage y Domain Shader.
Hull Shader
Este shader recibe como inputs los puntos de control y genera como output puntos de control que definen un patch (superficie) a subdividir junto con parámetros constantes por cada patch relativos a la forma en que se realiza la subdivisión.
Patch Constant Function
Para definir los parámetros de tessellation o cualquier otro output constante por patch se utiliza una función que se ejecuta por patch y tiene como input todos los puntos de control originales. La información que posee cada punto y la cantidad de puntos que conforman al patch se define en el header de la función, más precisamente en el input del tipo InputPatch.
struct HS_OUTPATCH { float Edges[3] : SV_TessFactor; float Inside : SV_InsideTessFactor; }; HS_OUTPATCH HSPatchFunction(InputPatch<HS_INPOSTXR, 3> PatchPoints, uint PatchID : SV_PrimitiveID) { HS_OUTPATCH Out; Out.Edges[0] = Out.Edges[1] = Out.Edges[2] = Out.Inside = tessFactor; return Out; }
Mínimamente, la salida debe definir SV_TessFactor y SV_InsideTessFactor, y el índice de cada array asociado dependerá del dominio (tipo de patch). Por ejemplo, un triángulo requiere de 3 SV_TessFactor (uno por lado) y un SV_InsideTessFactor, mientras que un quad de 4 y 2 respectivamente.
Estos factores determinan la cantidad de subdivisiones a realizar. Cabe mencionar que si algún factor es igual a 0 el patch no pasa a las etapas siguientes, por lo que se puede realizar algún tipo de culling en esta etapa.
Backface culling
Si el patch/triángulo no se encuentra mirando a la cámara, se setea el factor de tessellation en 0 para que sea descartado en la próxima etapa y evitar subdividir algo que de todas formas no se vería finalmente. Básicamente, si el producto escalar entre la normal de la superficie y el vector superficie->cámara es mayor a 0 (en realidad un epsilon muy pequeño), se descarta. Google lo explica mejor.
HS_OUTPATCH HSPatchFunctionBFCulling(InputPatch<HS_INPOSTXR, 3> PatchPoints, uint PatchID : SV_PrimitiveID) { HS_OUTPATCH Out; float3 patchNormal = normalize(cross(PatchPoints[1].Pos - PatchPoints[0].Pos, PatchPoints[2].Pos - PatchPoints[0].Pos)); if (BackFaceCull(dot(patchNormal, normalize(PatchPoints[0].Pos - viewPos)), dot(patchNormal, normalize(PatchPoints[1].Pos - viewPos)), dot(patchNormal, normalize(PatchPoints[2].Pos - viewPos)), 0.1)) Out.Edges[0] = Out.Edges[1] = Out.Edges[2] = Out.Inside = 0; else Out.Edges[0] = Out.Edges[1] = Out.Edges[2] = Out.Inside = tessFactor; return Out; }
Comparación entre Backface culling ON (izquierda) y OFF (derecha) seteando el rasterizer en CullNone:
Shader
EL hull shader propiamente dicho genera cada punto de control de salida a partir de la información relativa a todos los puntos de control de input y un ID del punto de control a generar (interno al patch). Además, se deben configurar parámetros requeridos por la Tessellation Stage que indican cómo realizar la subdivision.
[domain("tri")] [partitioning("integer")] [outputtopology("triangle_cw")] [outputcontrolpoints(3)] [patchconstantfunc("HSPatchFunction")] HS_OUTPOINTPOSTXR HSTessellation(InputPatch<HS_INPOSTXR, 3> PatchPoints, uint PatchID : SV_PrimitiveID, uint PointID : SV_OutputControlPointID) { HS_OUTPOINTPOSTXR Out; Out.Pos = PatchPoints[PointID].Pos; Out.Txr = PatchPoints[PointID].Txr; return Out; }
domain indica el tipo de patch de entrada, partitioning el algoritmo a utilizar para subdividir, outputtopology el tipo de primitiva de salida, outputcontrolpoints la cantidad de control points de salida, y patchconstantfunc el nombre de la función que genera la información por patch.
Tessellator Stage
Esta etapa se encarga de realizar la subdivisión de los patchs. Es fixed, por lo que no se programa nada y sólo se setean los parámetros en el HS (msdn).
Domain Shader
A nivel funcionalidad se podría pensar como el Vertex Shader si las primitivas generadas hubiesen sido directamente mandadas como input del pipeline y no hubiese tessellation. En el DS se calcula la posición final de cada vértice (output point) generado por la TS utilizando como entrada la ubicación de los puntos de control (generada por el HS), la información constante per patch (generada por la patch constant function del HS), y las coordenadas del vértice generada dentro del patch.
Estas coordenadas se obtienen a partir de la system value SV_DomainLocation y su tipo de dato depende del tipo de patch. Para un quad, se utiliza un float2 que indica las coordenadas internas UV (símil coordenadas de textura), para un triángulo (el caso de ejemplo que hice) un float3 que representa las coordenadas baricéntricas, y para una línea un float2 que no sé que representa.
[domain("tri")] PS_INPOSTXR DSTessellation (HS_OUTPATCH PatchData, float3 BariPos : SV_DomainLocation, const OutputPatch<HS_OUTPOINTPOSTXR, 3> PatchPoints) { PS_INPOSTXR Out; //Interpolación baricéntrica en función de los 3 control points (vértices del triángulo/patch original) float4 pos = float4(PatchPoints[0].Pos * BariPos.x + PatchPoints[1].Pos * BariPos.y + PatchPoints[2].Pos * BariPos.z, 1.0f); Out.Pos = mul(pos, matViewProjection); Out.Txr = PatchPoints[0].Txr * BariPos.x + PatchPoints[1].Txr * BariPos.y + PatchPoints[2].Txr * BariPos.z; return Out; }
Video
Coming up
Lo próximo que haga sea probablemente displacement mapping. Parece interesante y relativamente simple.
SlimDX y DirectX 11 - AMD GPUPerfAPI
Código fuente y ejecutable de la solución (para que GPA funcione, la aplicación debe ejecutarse como administrador)
Advertencia
Colgué un par de veces la PC mientras probaba algunos counters (muchos a la vez) con Fraps al mismo tiempo :P
GPUPerfAPI
Biblioteca de AMD para analizar performance en placas de video AMD/ATI. Overview y user guide (bastante completa).
GPAWrapper.cs
Importé todas las funciones que figuran en GPUPerfAPI.h y agregué una función para leer los datos de una sesión y formatearlos según su tipo y usage.
namespace Framework { [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void GPA_Logging_Delegate(GPA_Logging_Type logType, string mensaje); /// /// Status enumerations /// public enum GPA_Status { GPA_STATUS_OK = 0, GPA_STATUS_ERROR_NULL_POINTER, GPA_STATUS_ERROR_COUNTERS_NOT_OPEN, GPA_STATUS_ERROR_COUNTERS_ALREADY_OPEN, GPA_STATUS_ERROR_INDEX_OUT_OF_RANGE, GPA_STATUS_ERROR_NOT_FOUND, GPA_STATUS_ERROR_ALREADY_ENABLED, GPA_STATUS_ERROR_NO_COUNTERS_ENABLED, GPA_STATUS_ERROR_NOT_ENABLED, GPA_STATUS_ERROR_SAMPLING_NOT_STARTED, GPA_STATUS_ERROR_SAMPLING_ALREADY_STARTED, GPA_STATUS_ERROR_SAMPLING_NOT_ENDED, GPA_STATUS_ERROR_NOT_ENOUGH_PASSES, GPA_STATUS_ERROR_PASS_NOT_ENDED, GPA_STATUS_ERROR_PASS_NOT_STARTED, GPA_STATUS_ERROR_PASS_ALREADY_STARTED, GPA_STATUS_ERROR_SAMPLE_NOT_STARTED, GPA_STATUS_ERROR_SAMPLE_ALREADY_STARTED, GPA_STATUS_ERROR_SAMPLE_NOT_ENDED, GPA_STATUS_ERROR_CANNOT_CHANGE_COUNTERS_WHEN_SAMPLING, GPA_STATUS_ERROR_SESSION_NOT_FOUND, GPA_STATUS_ERROR_SAMPLE_NOT_FOUND, GPA_STATUS_ERROR_SAMPLE_NOT_FOUND_IN_ALL_PASSES, GPA_STATUS_ERROR_COUNTER_NOT_OF_SPECIFIED_TYPE, GPA_STATUS_ERROR_READING_COUNTER_RESULT, GPA_STATUS_ERROR_VARIABLE_NUMBER_OF_SAMPLES_IN_PASSES, GPA_STATUS_ERROR_FAILED, GPA_STATUS_ERROR_HARDWARE_NOT_SUPPORTED, } /// /// Value type definitions /// public enum GPA_Type { GPA_TYPE_FLOAT32, // Result will be a 32-bit float GPA_TYPE_FLOAT64, // Result will be a 64-bit float GPA_TYPE_UINT32, // Result will be a 32-bit unsigned int GPA_TYPE_UINT64, // Result will be a 64-bit unsigned int GPA_TYPE_INT32, // Result will be a 32-bit int GPA_TYPE_INT64, // Result will be a 64-bit int GPA_TYPE__LAST // Marker indicating last element } /// /// Result usage type definitions /// public enum GPA_Usage_Type { GPA_USAGE_TYPE_RATIO, // Result is a ratio of two different values or types GPA_USAGE_TYPE_PERCENTAGE, // Result is a percentage, typically within [0,100] range, but may be higher for certain counters GPA_USAGE_TYPE_CYCLES, // Result is in clock cycles GPA_USAGE_TYPE_MILLISECONDS, // Result is in milliseconds GPA_USAGE_TYPE_BYTES, // Result is in bytes GPA_USAGE_TYPE_ITEMS, // Result is a count of items or objects (ie, vertices, triangles, threads, pixels, texels, etc) GPA_USAGE_TYPE_KILOBYTES, // Result is in kilobytes GPA_USAGE_TYPE__LAST // Marker indicating last element } /// /// Logging type definitions /// public enum GPA_Logging_Type { GPA_LOGGING_NONE = 0, GPA_LOGGING_ERROR = 1, GPA_LOGGING_MESSAGE = 2, GPA_LOGGING_ERROR_AND_MESSAGE = 3, GPA_LOGGING_TRACE = 4, GPA_LOGGING_ERROR_AND_TRACE = 5, GPA_LOGGING_MESSAGE_AND_TRACE = 6, GPA_LOGGING_ERROR_MESSAGE_AND_TRACE = 7, GPA_LOGGING_ALL = 0xFF } /// /// See http://developer.amd.com/tools/GPUPerfAPI/assets/GPUPerfAPI-UserGuide.pdf /// public static class GPAWrapper { [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_Initialize(); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_Destroy(); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_OpenContext(IntPtr pointerToDevice); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_CloseContext(); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_SelectContext(IntPtr pointerToDevice); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_GetNumCounters(out uint counterIndicesList); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_GetCounterName(uint counterIndex, out string counterName); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_GetCounterIndex(string counterName, out uint counterIndex); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_GetCounterDescription(uint counterIndex, out string counterDescription); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_GetCounterDataType(uint counterIndex, out GPA_Type counterDataType); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_GetDataTypeAsStr(GPA_Type counterDataType, out string counterDataTypeStr); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_GetCounterUsageType(uint counterIndex, out GPA_Usage_Type counterUsageType); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_GetUsageTypeAsStr(GPA_Usage_Type counterUsageType, out string counterUsageTypeStr); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_EnableCounter(uint counterIndex); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_EnableCounterStr(string counterName); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_EnableAllCounters(); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_GetEnabledCount(out uint enabledCount); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_GetEnabledIndex(uint enabledIndex, out uint counterIndex); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_IsCounterEnabled(uint counterIndex); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_DisableCounter(uint counterIndex); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_DisableCounterStr(string counterName); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_DisableAllCounters(); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_GetPassCount(out uint passCount); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_BeginSession(out uint sessionID); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_EndSession(); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_BeginPass(); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_EndPass(); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_BeginSample(uint sampleID); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_EndSample(); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_IsSessionReady(out bool isReady, uint sessionID); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_IsSampleReady(out bool isReady, uint sessionID, uint sampleID); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_GetSampleUInt32(uint sessionID, uint sampleID, uint counterIndex, out uint sampledValue); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_GetSampleUInt64(uint sessionID, uint sampleID, uint counterIndex, out ulong sampledValue); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_GetSampleFloat32(uint sessionID, uint sampleID, uint counterIndex, out float sampledValue); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_GetSampleFloat64(uint sessionID, uint sampleID, uint counterIndex, out double sampledValue); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_GetSampleCount(uint sessionID, out uint samplesCount); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern string GPA_GetStatusAsStr(GPA_Status gpaStatus); [DllImport("GPUPerfAPIDX11.dll", CallingConvention = CallingConvention.Cdecl)] public static extern GPA_Status GPA_RegisterLoggingCallback(GPA_Logging_Type gpaLoggingType, GPA_Logging_Delegate loggingFunction); /// /// Queries the last samples taken in a session /// ///Session ID ///Array of counter indices ///Array with the data type associated with each counter index of counters ///Array with the usage type associated with each counter index of counters /// An array of string formatted values public static string[] SamplesToString(uint sessionID, uint[] counters, GPA_Type[] types, GPA_Usage_Type[] usages) { string[] outputValues = new string[counters.Length]; uint sampleIndex; GPAWrapper.GPA_GetSampleCount(sessionID, out sampleIndex); sampleIndex--; for (int j = 0; j < counters.Length; j++) { switch (types[j]) { case GPA_Type.GPA_TYPE_FLOAT32: float floatValue; GPAWrapper.GPA_GetSampleFloat32(sessionID, sampleIndex, counters[j], out floatValue); outputValues[j] = string.Format("{0:0.###}", floatValue); break; case GPA_Type.GPA_TYPE_FLOAT64: double doubleValue; GPAWrapper.GPA_GetSampleFloat64(sessionID, sampleIndex, counters[j], out doubleValue); outputValues[j] = string.Format("{0:0.###}", doubleValue); break; case GPA_Type.GPA_TYPE_UINT32: uint uintValue; GPAWrapper.GPA_GetSampleUInt32(sessionID, sampleIndex, counters[j], out uintValue); outputValues[j] = uintValue.ToString(); break; case GPA_Type.GPA_TYPE_UINT64: ulong uint64Value; GPAWrapper.GPA_GetSampleUInt64(sessionID, sampleIndex, counters[j], out uint64Value); outputValues[j] = uint64Value.ToString(); break; } switch (usages[j]) { case GPA_Usage_Type.GPA_USAGE_TYPE_BYTES: outputValues[j] += " bytes"; break; case GPA_Usage_Type.GPA_USAGE_TYPE_CYCLES: outputValues[j] += " cycles"; break; case GPA_Usage_Type.GPA_USAGE_TYPE_ITEMS: outputValues[j] += " items"; break; case GPA_Usage_Type.GPA_USAGE_TYPE_KILOBYTES: outputValues[j] += " Kbytes"; break; case GPA_Usage_Type.GPA_USAGE_TYPE_MILLISECONDS: outputValues[j] += " ms"; break; case GPA_Usage_Type.GPA_USAGE_TYPE_PERCENTAGE: outputValues[j] += " %"; break; case GPA_Usage_Type.GPA_USAGE_TYPE_RATIO: outputValues[j] += " ratio"; break; } } return outputValues; } } }
GPA brinda la posibilidad de setear una callback function a la cual la biblioteca invoca con los distintos errores y mensajes de logueo. Para mi ejemplo, hice una función que graba en GPALog.txt sólo los errores y advertencias.
Cómo usarlo
Habilitar GPA en el checkbox Enable GPUPerfAPI y agregar los counters que uno quiera medir en el datagrid que está debajo.
Para otros proyectos, sólo hace falta copiar GPAWrapper.cs y llamar a la biblioteca como uno quiera (por ejemplo, leyendo la data después de terminada la aplicación para no disminuir la performance).
Limitaciones
Desde el punto de vista de funcionalidad importada, ninguna.
Por otro lado, algunos counters requieren de más de una pasada de exactamente el mismo frame para poder obtener sus resultados, por lo que si el usuario selecciona un conjunto de counters que requiere de más de una pasada para samplear los datos, se desactiva GPA y se muestra una advertencia.
SlimDX y DirectX 11 - Compute Shader, Raw Buffer y Unordered Access View
Código fuente y ejecutable de la solución
Colisión Box-Line segment
Antes que nada, fe de erratas del post anterior. Arreglé el testeo de colisión para que rebote como es debido :P El código de la función intersects es una adaptación de este.
Aclaración
Todo lo que escribo termina siendo un mini y propenso a errores resumen de lo que figura en msdn, por lo que para entender realmente las cosas recomiendo leer de ahí. Compute Shader
El CS permite aprovechar el paralelismo y la capacidad de procesamiento de datos de la GPU para aplicaciones de todo tipo (GPGPU). De hecho, no figura como una etapa del graphics pipeline de DX 11 y lo único que comparte con los demás shaders es su sintaxis en HLSL.
Threads
La ejecución de un CS dispara la creación de threads agrupados en grupos (duplica) con un sector de memoria compartida. Cada thread tiene asociado un ID de 3 dimensiones que lo identifica dentro de su grupo, mientras que cada grupo tiene a su vez un ID de 3 dimensiones que lo distingue de los demás. El límite de threads por grupo es de 1024 (msdn). Para definir sobre qué dato trabajará cada thread, probablemente se necesite tener acceso desde el código al ID del thread y/o grupo, por lo que existen System-Value Semantics que brindan esa información:
SV_GroupID: offset del grupo respecto del total dispatcheado, por dimensión. SV_GroupThreadID: offset del thread respecto de su grupo, por dimensión. SV_GroupIndex: offset del thread respecto de su grupo, unidimensional (de 0 a (numthreadsX * numthreadsY * numThreadsZ) – 1). SV_DispatchThreadID: offset del thread respecto del total dispatcheado, por dimensión (SV_GroupID * dimensionesDeCadaGrupo + SV_GroupThreadID).
La siguiente imagen sacada de, como no podía ser de otra manera, msdn lo explica bastante bien:
Sintaxis
La sintaxis es simple y lo único notorio es cómo se define la cantidad de threads por grupo:
[numthreads(32, 32, 1)] void BasicComputeShader(uint3 threadID : SV_DispatchThreadID) { Cálculos... }
Con numthreads estamos definiendo que se crearán 32 * 32 * 1 = 1024 threads y cómo se repartirán dentro del grupo según las dimensiones (SV_GroupThreadID será (0...31, 0...31, 0)).
Ejecución
contexto.Dispatch(16, 16, 1);
El método Context.Dispatch(cantGruposX, cantGruposY, cantGruposZ) invoca al CS que, considerando los valores de numthreads de arriba, se ejecutaría para 16 * 16 * 1 grupos y 32 * 32 * 1 threads por grupo dando un total de 262144 threads.
Raw Buffer o Byte Address Buffer
Un recurso visto como Raw Buffer no tiene un tipo de dato definido sino que es simplemente un array de bytes, por lo que se almacena y lee su información de a 4 bytes (uint) especificando el offset en bytes del uint a leer/escribir.
Creación
Es interesante mencionar que lo que define el acceso como Raw es la vista creada sobre el buffer, por lo que el mismo puede ser accedido como Raw en función de una vista y luego como cualquier otro tipo (excepto Structured) por otra vista. Es decir, al momento de la creación del buffer uno sólo define la posibilidad de que el mismo sea accedido por una vista Raw de la siguiente forma:
unBuffer = new Buffer(..., ResourceOptionFlags.RawBuffer, ...);
Acceso
Las funciones más importantes son las de Load(uint address) y Store(uint address, uint value) y sus overloads para N-elementos. La primera lee 4 bytes según el offset en bytes dentro del buffer definido por el parámetro address y la segunda almacena un valor de 4 bytes (value) en la posición en bytes dada por address. Si uno necesita trabajar con otro tipo de dato que no sea uint, simplemente debe convertir los valores después de leerlos y antes de almacenarlos:
RWByteAddressBuffer Position; RWByteAddressBuffer Velocity; ... float3 position = asfloat(Position.Load3(buffersOffset)); float3 velocity = asfloat(Velocity.Load3(buffersOffset)); ... Position.Store3(buffersOffset, asuint(newPosition)); Velocity.Store3(buffersOffset, asuint(velocity));
Hay más funciones para trabajar sobre el buffer que yo no necesité y que se pueden revisar en RWByteAddressBuffer.
Habría estado lindo poder usar un Structured o Read/Write Buffer (es más simple su acceso), pero como el CS necesitaba tener acceso a buffers que luego eran utilizados como input del IA (es decir, habían sido creados con BindFlag.VertexBuffer), no fue posible. Ciertos BindFlags y ResourceOptionFlags son mutuamente excluyentes, como en el caso de BindFlag.VertexBuffer y ResourceOptionFlags.Structured.
Unordered Access View
Para aprovechar al máximo el paralelismo de los threads, es indispensable que los mismos trabajen sobre recursos que permitan un acceso simultáneo a los datos. DX 11 incorpora varios tipos de Resources y Views de los mismos según los permisos y la funcionalidad que uno requiera.
El tipo de vista Unordered Access View permite acceso de lectura/escritura simultáneo por distintos threads y sólo puede ser utilizado en Pixel Shaders o Compute Shaders.
Para que un recurso pueda ser asociado a una UAV, debe ser creado con el flag BindFlag.UnorderedAccess:
unBuffer = new Buffer(..., BindFlags.UnorderedAccess | BindFlags.VertexBuffer, ...);
Creación
Para crear una UAV hace falta definir el recurso sobre el cual recae la misma y la forma de interpretar los datos contenidos en el mismo a través de una descripción. Para el ejemplo, los recursos fueron creados con ResourceOptionFlags.RawBuffer y las vistas a crear serán también del tipo Raw.
UnorderedAccessViewDescription uavDesc = new UnorderedAccessViewDescription() { Format = SlimDX.DXGI.Format.R32_Typeless, Dimension = UnorderedAccessViewDimension.Buffer, Flags = UnorderedAccessViewBufferFlags.RawData, FirstElement = 0, ElementCount = CANTQUADS * 3, }; uavVertices = new UnorderedAccessView(Recursos.Device, vertexbuffer, uavDesc); uavVelocity = new UnorderedAccessView(Recursos.Device, velocitybuffer, uavDesc);
Dimension define el tipo de recurso y, al trabajar con RawData, el Format debe ser R32_Typeless (coincide con el acceso de a 4 bytes). Hay más opciones que no vienen al caso (Slices).
Bindeo al shader
public static void SetearUAV(this Effect inEffect, string inParameter, UnorderedAccessView inValue) { inEffect.GetVariableByName(inParameter).AsUnorderedAccessView().SetView(inValue); }
CSParticlesAndCollisions2D
Este programa es el análogo a GSParticlesAndCollisions2D (no explico todo otra vez, sino sólo las diferencias por lo que puede hacer falta ver el post anterior), con la diferencia de que la actualización de la posición y velocidad de los puntos se realiza en un Compute Shader en lugar de un Geometry Shader.
SlimDX
Al no actualizar desde un GS (y, por ende, no utilizar Stream Output) no hace falta hacer ping pong entre distintos buffers por lo que sólo son necesarios los buffers vertexbuffer y velocitybuffer.
En cada frame, primero se actualizan las partículas usando el CS:
effectCS.SetearUAV("Position", uavVertices); effectCS.SetearUAV("Velocity", uavVelocity); int pass = effectCS.GetTechniqueByName("UpdatePoints").Description.PassCount; for (int i = 0; i < pass; i++) { effectCS.GetTechniqueByName("UpdatePoints").GetPassByIndex(i).Apply(contexto); ////Dado que levanto 1024 threads por grupo (definido en el .fx), la cantidad de grupos a levantar es la cantidad total de partículas dividida por la cantidad de partículas(=threads) por grupo contexto.Dispatch(CANTQUADS / 1024, 1, 1); } contexto.ComputeShader.SetUnorderedAccessViews(nullUAVs, 0, 2);
Es importante la última línea que desasocia las vistas del shader antes de pasar al renderizado y usar los buffers como input:
contexto.InputAssembler.PrimitiveTopology = PrimitiveTopology.PointList; contexto.InputAssembler.InputLayout = layoutPointToQuad; contexto.InputAssembler.SetVertexBuffers(0, bindingPointToQuad); int passes = efectoPointToQuad.GetTechniqueByName("GSExpandPointToQuad").Description.PassCount; for (int i = 0; i < passes; i++) { efectoPointToQuad.GetTechniqueByName("GSExpandPointToQuad").GetPassByIndex(i).Apply(contexto); contexto.Draw(CANTQUADS, 0); } contexto.InputAssembler.SetVertexBuffers(0, nullbinding);
Shader de Update
El efecto se compila dentro de una technique11 como los demás, sólo que no puede haber otra stage en la técnica:
technique11 UpdatePoints { pass { SetComputeShader(CompileShader(cs_5_0, CSUpdatePoints())); } }
En cuanto al código, cada buffer almacena un vector de 3 dimensiones (uno posición y otro velocidad), por lo que es necesario leer de a 12 bytes (tamaño de 3 floats) usando la función Load3():
RWByteAddressBuffer Position; RWByteAddressBuffer Velocity; [numthreads(32, 32, 1)] void BasicComputeShader(uint3 threadID : SV_DispatchThreadID) { int buffersOffset = (threadID.x * 32 + threadID.y) * 4 * 3; float3 position = asfloat(Position.Load3(buffersOffset)); float3 velocity = asfloat(Velocity.Load3(buffersOffset)); Actualización y detección de colisiones... Position.Store3(buffersOffset, asuint(newPosition)); Velocity.Store3(buffersOffset, asuint(velocity)); }
Buffer offset
En cuanto al offset a utilizar para acceder al buffer, lo mejor es verlo a través de un ejemplo (tener en cuenta que cada thread actualiza a una sola partícula):
Cantidad de partículas: 16 Cantidad de threads por grupo: 4 Dispatch: (4,1,1) Dimensiones de cada grupo: (2,4,1)
Para empezar, estamos despachando 4 grupos de 8 threads c/u, lo que genera un total de 32 que actualizarán a 32 partículas. Siendo que son 32 partículas y cada buffer guarda 3 floats por partícula, el tamaño de cada uno será 32 * 3 * 4 bytes = 384 bytes.
Según las dimensiones del dispatch, el valor de SV_GroupID será (0,0,0) para el grupo 0, (1,0,0) para el grupo 1, (2,0,0) para el 2 y (3,0,0) para el 3. Según las dimensiones de cada grupo, los valores de SV_GroupThreadID serán ((0,0,0), (0,1,0), (0,2,0), (0,3,0), (1,0,0), (1,1,0), (1,2,0), (1,3,0). A partir de estas dos consideraciones y la fórmula de obtención de SV_DispatchThreadID, vemos que este valor para todos los threads del grupo 0 irá de (0,0,0) * (2,4,0) + (0,0,0) a (0,0,0) * (2,4,0) + (1,3,0). Para el grupo 1: (1,0,0) * (2,4,0) + (0,0,0) a (1,0,0) * (2,4,0) + (1,3,0) = (2,0,0) a (3,3,0). Para el grupo 2: (4,0,0) a (5,3,0). Y para el grupo 3: (6,0,0) a (7,3,0).
Por lo que la fórmula queda como (SV_DispatchThreadID.x * dimensionDeY + SV_DispatchThreadID.y) * 12
Compute vs Geometry
262144 partículas (lo que limita los FPS es el renderizado de los quads por lo que no tiene mucho sentido la comparación)
Geometry Shader
Compute Shader
1048576 partículas (sólo actualización, sin renderizado)
Geometry Shader
Compute Shader
Coming Up
Estoy importando GPUPerfAPI de AMD a SlimDX usando PInvoke. Si funciona (hasta ahora sólo probé preparar el device y chequear los counters que soporta), voy a estar escribiendo sobre esto en el próximo post.
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).
SlimDX y DirectX 11 - Point to Quad Geometry Shader
Solución
Para no armar una solución/proyecto por cada cosa que haga en SlimDX, hice un form Base.cs desde donde se elige y lanza (creando un thread) la prueba a ejecutar. De paso la uso para mostrar stats y demás sin recurrir a DirectWrite :P
Recién me entero de que activando el unmanaged code debugging en las propiedades del proyecto se pueden ver en la ventana de Output warnings y mensajes de error de DX, por lo que probablemente más adelante me proponga como tarea arreglar el código para no dejar recursos colgando :P
El código lo subo en el próximo post (igual copio textual el GS en este).
Pipeline de DX 10
Geometry Shader
Shader que trabaja a nivel de primitiva, el cual permite crear nuevas primitivas y/o descartarlas. Su input es un array de outputs del Domain Shader (Vertex Shader si no hubiere HS-Tesselator-DS) (dado que para trabajar sobre un triángulo, por ejemplo, requeriría de sus 3 vértices), y su output uno o más buffers símiles a los vertex buffers.
[maxvertexcount(4)] void GShaderGSExpandPointToQuad(point GS_INPOS Input[1], inout TriangleStream<PS_INPOSTXR> outStream) { Shader code... }
El header es bastante simple. Se define la cantidad máxima de vértices de salida por ejecución del GS (maxvertexcount), el tipo de primitiva de entrada (point) con su struct asociada, la cantidad de vértices a leer antes de ejecutar el GS (por ser point: Input[1]), y el tipo de primitiva de salida (TriangleStream).
Ejemplo de GS: Point to Quad/Sprite
Es casi el Hello World de los GSs. Ni siquiera hace uso del Stream Output (sobre el cual voy a escribir en el próximo post).
Este GS recibe un array de puntos y, por cada uno, genera un Triangle Strip formado por 2 triángulos que le dan forma a un Screen Aligned Quad. Cada vértice de salida se agrega al Stream utilizando la función Append(vertex) y, si bien en este shader no hace falta, si hiciese falta resetear el Strip de salida se usaría la función RestartStrip().
[maxvertexcount(4)] void GShaderGSExpandPointToQuad(point GS_INPOS Input[1], inout TriangleStream<PS_INPOSTXR> outStream) { PS_INPOSTXR Output; float fQuadDimensionX = fQuadDimensionsCS.x * (cos(Input[0].Pos.z + fTimeTotal) + 1.0); float fQuadDimensionY = fQuadDimensionsCS.y * (cos(Input[0].Pos.z + fTimeTotal) + 1.0); Output.Pos.z = 0.0; Output.Pos.w = 1.0; Output.Col = Input[0].Col; Output.Pos.xy = Input[0].Pos.xy + float2(-fQuadDimensionX, fQuadDimensionY); Output.Txr = float2(0.0,0.0); outStream.Append(Output); Output.Pos.xy = Input[0].Pos.xy + float2(-fQuadDimensionX, -fQuadDimensionY); Output.Txr = float2(0.0,1.0); outStream.Append(Output); Output.Pos.xy = Input[0].Pos.xy + float2(fQuadDimensionX, fQuadDimensionY); Output.Txr = float2(1.0,0.0); outStream.Append(Output); Output.Pos.xy = Input[0].Pos.xy + float2(fQuadDimensionX, -fQuadDimensionY); Output.Txr = float2(1.0,1.0); outStream.Append(Output); } technique11 GSExpandPointToQuad { pass P0 { SetVertexShader(CompileShader(vs_5_0, VShaderGSExpandPointToQuad())); SetGeometryShader(CompileShader(gs_5_0, GShaderGSExpandPointToQuad())); SetPixelShader(CompileShader(ps_5_0, PShaderGSExpandPointToQuad())); } }
Por cada punto (Input[0]), voy creando cada vértice del quad usando la posición y el tamaño en Clip Space y agregándolo al Stream. Pos.z almacena un valor random generado al momento de crear los vertex buffers que modifica la fase de expansión del quad para que no estén todos sincronizados :P
Del lado de la aplicación no hay que codificar absolutamente nada ya que el GS se carga al cargar el efecto entero (si no se usase Effect habría que compilarlo usando new GeometryShader()).
Lo que voy a estar posteando en cuanto acomode un poco el código y le agregue colisión en 3D contra volúmenes simples: Geometry Shader y Stream Output.
Una pasada de GS que actualiza la posición y velocidad de cada partícula (punto) y devuelve los buffers usando el SO, para luego mandar los buffers actualizados a una pasada con un GS que genera un quad por cada punto.
Se ve horrible porque usé una textura horrible y asimétrica y porque los quads se agrandan y achican en función del tiempo :P.
SlimDX y DirectX 11 - Query
Algo simple pero bastante útil.
Una query se basa en obtener stats del pipeline desde la placa de video como, por ejemplo, invocaciones a cada shader, primitivas renderizadas, tiempo transcurrido, etc. Hay una query en particular denominada occlusion query que es sumamente útil para occlusion culling (al menos eso tengo entendido: GPU Gems).
El encapsulamiento que hace SlimDX del manejo de las queries mapea prácticamente uno a uno contra las llamadas a la API de DX11, por lo que con la documentación de msdn alcanza y sobra para poder trabajar.
PipelineStatistics
Para el ejemplo, voy a utilizar una query que brinda datos generales sobre cada etapa del pipeline.
pipelineStatisticsQuery = new Query(device, new QueryDescription(QueryType.PipelineStatistics, QueryFlags.None));
El intervalo en el que se obtienen los datos de la query se define mediante los métodos Begin(query) y End(query). Algunas, sin embargo, sólo requieren de End(query).
context.Begin(pipelineStatisticsQuery); prueba.Render(elapsed); context.End(pipelineStatisticsQuery); swapChain.Present(0, PresentFlags.None);
Una vez obtenida la información, se debe verificar si la misma está disponible para luego proceder a su lectura:
while (!context.IsDataAvailable(pipelineStatisticsQuery)) ; PipelineStatistics pipelineStatistics = context.GetData<PipelineStatistics>(pipelineStatisticsQuery);
Si bien esto funciona, el usar un while para esperar a que se pueda leer el resultado de la query en cada frame disminuye bastante la performance. Me parece más sensato desacoplar el ciclo de actualización de la query respecto del de renderizado:
if (queryIsReadable) context.Begin(pipelineStatisticsQuery); prueba.Render(elapsed); if (queryIsReadable) context.End(pipelineStatisticsQuery); swapChain.Present(0, PresentFlags.None); if (context.IsDataAvailable(pipelineStatisticsQuery)) { pipelineStatistics = context.GetData<PipelineStatistics>(pipelineStatisticsQuery); queryIsReadable = true; } else queryIsReadable = false;
Así, una vez capturada la información, se intenta en cada ciclo leer la misma y no se vuelve a capturar hasta no lograrlo. Notar qué pasaría si directamente se reemplazase el while inicial por un if: nunca estaría disponible para leer porque, si bien no se bloquearía esperando, en cada ciclo se volvería a capturar y procesar.
Código fuente
El código fuente lo subo en el próximo post, en donde voy a mostrar mi primera incursión en Geometry Shaders con un shader que simplemente expande puntos a quads.
SlimDX y DirectX 11 - Normal Mapping y múltiples Point Lights
Código fuente y ejecutable de la solución
Controles
WASDQE: Mueve la cámara.
TFGHRY: Mueve las tres luces a la vez.
Shift + TFGHRY: Mueve la luz roja.
Ctrl + TFGHRY: Mueve la luz verde.
Alt + TFGHRY: Mueve la luz azul.
UJ: Rota a Altair.
IK: Escala a Altair.
1: Habilita/deshabilita Frustum Culling.
2: Habilita/deshabilita renderizar las Bounding Spheres.
3: Alterna entre distintos shaders para Altair y los cubos.
Legibilidad del código
Para hacer un poco más entendible el código, agregué un summary por cada método (en inglés) y le pasé Stylecop a la solución. Tal vez ahora sí se pueda entender aunque sea algo.
/// <summary> /// Rotates (adds to the actual rotation) the model given an arbitrary axis and angle /// </summary> /// <param name="inAxis">Rotation axis</param> /// <param name="inAngle">Rotation angle in radians</param>
Normal Mapping
Normal Mapping se basa en reemplazar la normal utilizada en las fórmulas de iluminación por una leída desde una textura (Normal Map). Esto permite simular relieve sin alterar la geometría del modelo.
Dado que el relieve es dependiente de la textura (Diffuse Map) sobre la que se aplica y no del objeto sobre el que se aplica esa textura, las normales almacenadas están en un espacio de coordenadas propio de la textura y no del objeto: Tangent Space.
Tangent Space
El Tangent Space es un sistema de coordenadas cuyos ejes son la normal de la superficie (normal), un versor cuya dirección es igual a la dirección en la que aumenta la coordenada de textura U (tangente) y un versor cuya dirección bla bla bla la coordenada de textura V (bitangente (también llamado binormal)). Hay mil explicaciones en Internet mejores que la que acabo de dar. :P
Para que se entienda un poco mejor, las siguientes imágenes muestran el valor de cada vector en WS para simples cubos. La imagen de arriba a la izquierda muestra una textura en la que se ve la dirección de los vectores U y V de cada cubo. A su derecha se muestran las normales en WS. Abajo a la izquierda los vectores tangentes (en WS), y a la derecha los bitangentes (en WS).
Por ejemplo, el vector tangente de la cara +Z del cuadrado de la derecha tiene la dirección de +Y (verde clarito), que coincide con la que figura para U en la textura.
Generación de tangentes y bitangentes
Dado que la normal a muestrear desde el normal map estará en TS, hará falta pasar los vectores Light Dir y Eye a TS, o la normal a WS. Cualquiera sea el caso, se va a necesitar una matriz compuesta por la base del TS que son los tres vectores ya mencionados.
El algoritmo para la generación de la tangente y bitangente de un vértice lo saqué de acá. De todas formas, seguramente haya hecho algo mal ya que tuve que modificar la obtención de la bitangente ortogonal para obtener el mismo resultado que muestra el shader de ejemplo Show Binormals de RenderMonkey.
while (line != null && line.StartsWith("f ")) { Vector2[] texcoordsFace = new Vector2[3]; Vector3[] verticesFace = new Vector3[3]; string[] indicesFaces = new string[3]; fields = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < 3; i++) { indicesFace = fields[i + 1].Split(new[] { '/' }, StringSplitOptions.None); indicesFaces[i] = indicesFace[0]; meshAux.Indices.Add(ReadInt(indicesFace[0]) - 1 - indexOffset); indAbsolutoTex[ReadInt(indicesFace[0]) - 1 - indexOffset] = ReadInt(indicesFace[1]) - 1; verticesFace[i] = meshAux.Positions[ReadInt(indicesFace[0]) - 1 - indexOffset]; texcoordsFace[i] = texcoordsTotales[ReadInt(indicesFace[1]) - 1]; } int i0 = ReadInt(indicesFaces[0]) - 1 - indexOffset; int i1 = ReadInt(indicesFaces[1]) - 1 - indexOffset; int i2 = ReadInt(indicesFaces[2]) - 1 - indexOffset; float a0 = AnguloDeVertice(verticesFace[0], verticesFace[1], verticesFace[2]); float a1 = AnguloDeVertice(verticesFace[1], verticesFace[2], verticesFace[0]); float a2 = AnguloDeVertice(verticesFace[2], verticesFace[0], verticesFace[1]); Vector3 v10 = verticesFace[0] - verticesFace[1]; Vector3 v12 = verticesFace[2] - verticesFace[1]; Vector3 normal = Vector3.Normalize(Vector3.Cross(v12, v10)); normalAcumuladaPorVertice[i0] += normal * a0; normalAcumuladaPorVertice[i1] += normal * a1; normalAcumuladaPorVertice[i2] += normal * a2; v10 = verticesFace[1] - verticesFace[0]; v12 = verticesFace[2] - verticesFace[0]; Vector2 texCoordsRelativas1 = texcoordsFace[1] - texcoordsFace[0]; Vector2 texCoordsRelativas2 = texcoordsFace[2] - texcoordsFace[0]; float unoSobreDeterminante = 1f / (texCoordsRelativas1.X * texCoordsRelativas2.Y - texCoordsRelativas2.X * texCoordsRelativas1.Y); Vector3 tangente, bitangente; tangente.X = v10.X * texCoordsRelativas2.Y - v12.X * texCoordsRelativas1.Y; tangente.Y = v10.Y * texCoordsRelativas2.Y - v12.Y * texCoordsRelativas1.Y; tangente.Z = v10.Z * texCoordsRelativas2.Y - v12.Z * texCoordsRelativas1.Y; tangente *= unoSobreDeterminante; bitangente.X = v10.X * texCoordsRelativas1.X - v12.X * texCoordsRelativas2.X; bitangente.Y = v10.Y * texCoordsRelativas1.X - v12.Y * texCoordsRelativas2.X; bitangente.Z = v10.Z * texCoordsRelativas1.X - v12.Z * texCoordsRelativas2.X; bitangente *= unoSobreDeterminante; tangenteAcumuladaPorVertice[i0] += tangente * a0; tangenteAcumuladaPorVertice[i1] += tangente * a1; tangenteAcumuladaPorVertice[i2] += tangente * a2; bitangenteAcumuladaPorVertice[i0] += bitangente * a0; bitangenteAcumuladaPorVertice[i1] += bitangente * a1; bitangenteAcumuladaPorVertice[i2] += bitangente * a2; line = reader.ReadLine(); } for (int i = 0; i < meshAux.Positions.Count; i++) { normalAcumuladaPorVertice[i].Normalize(); tangenteAcumuladaPorVertice[i].Normalize(); bitangenteAcumuladaPorVertice[i].Normalize(); /*Gram-Smichdt*/ tangenteOrto = Vector3.Normalize(tangenteAcumuladaPorVertice[i] - Vector3.Dot(normalAcumuladaPorVertice[i], tangenteAcumuladaPorVertice[i]) * normalAcumuladaPorVertice[i]); //float hand = (Vector3.Dot(Vector3.Cross(normalAcumuladaPorVertice[i], tangenteAcumuladaPorVertice[i]), bitangenteAcumuladaPorVertice[i]) < 0.0f) ? -1.0f : 1.0f; float hand = -1.0f; bitangenteOrto = Vector3.Normalize(Vector3.Cross(normalAcumuladaPorVertice[i], tangenteOrto) * hand); meshAux.TextureCoordinates.Add(texcoordsTotales[indAbsolutoTex[i]]); meshAux.Normals.Add(normalAcumuladaPorVertice[i]); meshAux.Tangents.Add(tangenteOrto); meshAux.Bitangents.Add(bitangenteOrto); //meshAux.Tangents.Add(tangenteAcumuladaPorVertice[i]); //meshAux.Bitangents.Add(bitangenteAcumuladaPorVertice[i]); }
Known bug: si el .obj primero define todos los vértices y luego todos los cuerpos en lugar de intercalar, todos los cuerpos de la escena tendrán el mismo vertex buffer, desperdiciando espacio y generando para cada uno una misma BoundingSphere que termina englobando a la escena entera. Nada terrible, más adelante lo arreglaré.
Lighting
El modelo de iluminación del shader se basa en el que trae embebido DX9 (msdn), con la diferencia de que no implementa emmisive lightning.
No hay mucha diferencia con el modelo para una sola luz, simplemente se debe calcular cada término de la fórmula por cada luz.
float4 ComputeAmbientLight(float4 materialColor, float4 lightColor, float attenuation) { return materialColor * lightColor * attenuation; } float4 ComputeDiffuseLight(float4 materialColor, float4 lightColor, float3 normal, float3 lightDirection, float attenuation) { return materialColor * lightColor * saturate(dot(normal, lightDirection)) * attenuation; } float4 ComputeSpecularLight(float4 materialColor, float4 lightColor, float3 normal, float3 lightDirection, float3 viewDirection, float attenuation) { float3 vReflect = reflect(-lightDirection, normal); return materialColor * lightColor * saturate(pow(max(0, dot(vReflect, viewDirection)), fMatSpecularPower)) * attenuation; } float4 PShaderTexturedNormalPointLight(VS_OUTPOSTXREYELUZ Input) : SV_TARGET { float3 normal = normalize(normalMap.Sample(samLinearWrap, Input.Txr).xyz * 2.0 - 1.0); float4 colorAD = float4(0,0,0,1); float4 colorS = float4(0,0,0,1); float3 lightDir; for (int i=0; i < iCantLights; i++) { lightDir = normalize(Input.LDir[i].xyz); colorAD += ComputeAmbientLight(cMatAmbient, cAmbient[i], Input.LDir[i].w) + ComputeDiffuseLight(cMatDiffuse, cDiffuse[i], normal, lightDir, Input.LDir[i].w); colorS += ComputeSpecularLight(cMatSpecular, cSpecular[i], normal, lightDir, Input.Eye, Input.LDir[i].w); } return diffuseMap.Sample(samLinearWrap, Input.Txr) * saturate(colorAD) + colorS; }
La clase PointLight que desarrollé almacena las constantes de la luz en un Constant Buffer para setear todos los parámetros a la vez, pero lamentablemente no existen los arrays de Constants Buffers por lo que, al trabajar con N luces, tuve que setear cada parámetro por separado (setear N * parámetros/luz parámetros, en lugar de setear N constant buffers).
Video
TP que hice para la materia Técnicas de Gráficos por Computadora en el 2010. Busca asemejarse a un CTF del Quake III (con algunos modelos del QII).
Código fuente y detalles de implementación de este trabajo y los de los demás alumnos en sites.google.com/site/tgcutn :P
Finalmente logré detectar los patrones sin importar su rotación. Si bien no es eficiente, es eficaz y completamente original (en el sentido de haberlo pensado desde cero).
1. Obtengo los bordes del área (que idealmente sería un rectángulo) y, por trigonometría, el ángulo de rotación.
2. Relativizo las posiciones de los puntos del área respecto del eje de rotación (que será el píxel más a la izquierda).
3. Transformo estás posiciones en función del ángulo para llevarlas a un rectángulo alineado con los ejes. El eje de rotación queda posicionado entonces en el (0,0).
4. Detecto si la posición transformada entra en el rectángulo en donde se encontraría el patrón ((0, 0) a (width, height)). Si no entra, descarto al píxel.
5. Escalo la posición a ((0,0) a (7,7)) y comparo el sampleo del patrón en esa posición con el color del píxel original.
SlimDX y DirectX 11 - Material.cs y Annotations
Ya tengo implementado normal mapping (se suponía que era simple, pero al momento de implementarlo me topé con que no tenía la mágica opción de XNA o RM de generar las tangentes y bitangentes de los vértices :P), pero antes de eso estuve haciendo algunos retoques a la solución.
Aclaro que el diseño de la misma deja mucho que desear (cohesión? ja!), simplemente busco que sea fácil de usar y modificar para adaptarlo a las técnicas a probar en un futuro.
Material.cs
Como el nombre lo indica, abarca todo lo relacionado al material de un modelo (o varios, si lo comparten). Junto con la implementación de esta clase, mejoré el ObjLoader.cs para leer del archivo .mtl parámetros como el normal map, los colores (en caso de no usar texture map), y el coeficiente de luz especular.
public string MaterialName {get; private set;} public Texture2D DiffuseMap {get; private set;} public Texture2D NormalMap {get; private set;} public Effect Effect {get; private set;} public string Technique { get; set; } public Color4 AmbientColor { get; private set; } public Color4 DiffuseColor { get; private set; } public Color4 SpecularColor { get; private set; } public float SpecularPower { get; private set; } public Material(string inName, Texture2D inDiffuseMap, Texture2D inNormalMap, float inSpecularPower); public Material(string inName, Texture2D inDiffuseMap, Texture2D inNormalMap); public Material(string inName, Texture2D inDiffuseMap, Texture2D inNormalMap, Color4 inAmbient, Color4 inDiffuse, Color4 inSpecular, float inSpecularPower); public bool EsOpaco; public int CantPasses; public ShaderSignature GetShaderSignature(); public string[] GetCurrentSemantics(); public void ApplyPass(int index, DeviceContext context); public void UpdateLightConstants(PointLight inLight); public void EnableLighting(); public void Dispose();
El método GetCurrentSemantics() merece una mención especial. Si uno modifica en tiempo real el shader a utilizar para un determinado material, muy probablemente deba modificar el input layout del modelo que usa ese material. Por ejemplo, un simple textured pong shader requeriría la posición, las coords de textura y la normal de cada vértice mientras que un normal mapping necesitaría la tangente y la bitangente también. GetCurrentSemantics() obtiene, para la técnica actual, las semánticas de entrada que necesita.
Por lo tanto, cada vez que se actualiza la técnica a utilizar por un renderizado, se debe regenerar inmediatamente después su layout asociado:
public void ChangeTechnique(string inTechnique) { material.Technique = inTechnique; layout.Dispose(); ReloadInputBindings(); } private void ReloadInputBindings() { string[] requiredSemantics = material.GetCurrentSemantics(); var device = CicloBase.Instance.device; elements = new InputElement[requiredSemantics.Length]; binding = new VertexBufferBinding[requiredSemantics.Length]; for (int i = 0; i < requiredSemantics.Length; i++) { int index = semantics.IndexOf(requiredSemantics[i]); if (index == -1) throw new NullReferenceException("No se cargaron los datos de semántica requerida por la technique"); elements[i] = new InputElement(requiredSemantics[i], 0, formats[index], i); binding[i] = new VertexBufferBinding(buffers[index], strides[index],0); } layout = new InputLayout(device, material.GetShaderSignature(), elements); }
Annotations
Lo realmente interesante del post es mostrar cómo obtengo las semánticas de una technique particular. Idealmente, se deberían poder leer directamente del recurso ShaderSignature, pero no encontré forma (si alguien conoce una, avise :P).
Mi idea fue entonces la de aprovechar las annotations (msdn) que se pueden realizar en el medio del código HLSL. Vienen a ser variables con un valor constante definido textualmente en el código que se pueden leer desde nuestra aplicación (no tienen uso para el shader). Pueden definirse anotaciones sobre variables (globales o locales), sobre técnicas, y creo que sobre passes también.
Opté por definir una annotation para cada técnica compuesta por un string con las semánticas separadas por espacios:
technique11 TexturedNormalPointLight <string SEMANTICS = "POSITION TEXCOORD NORMAL TANGENT BITANGENT";> { pass P0 { SetVertexShader(CompileShader(vs_4_0, VShaderTexturedNormalPointLight())); SetPixelShader(CompileShader(ps_4_0, PShaderTexturedNormalPointLight())); } }
Finalmente, desde mi aplicación leo la variable SEMANTICS y la parseo de la siguiente forma:
public string[] GetCurrentSemantics() { return Effect.GetTechniqueByName(Technique).GetAnnotationByName("SEMANTICS").AsString().GetString().Split(new char[] { ' ' }); }
Cambio de planes: voy a estar trabajando en reconocer en tiempo real dónde hay un código QR en una imagen cualquiera para un trabajo práctico de la materia Procesamiento de Señales.
El approach por ahora es binarizar la imagen, obtener las áreas conexas de píxeles y su rectángulo que limita a cada una, y testear sobre cada área el patrón buscado aceptando el resultado según un umbral de porcentaje de aciertos.
Los rectángulos verdes son los reconocidos por mi algoritmo como patrones de alineamiento de QR.