Shaders Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! En OpenGL antes de la versión 2.0 la línea de producción gráfica implementada por OpenGL era fija: se podían cambiar estados, pero los resultados eran muy restringidos, por ejemplo, no se podían aplicar efectos como normal mapping (bump mapping). ■ Por ello, a partir de OpenGL 2.0 se implemento la posibilidad de programar partes de la línea de producción gráfica. ■ Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! Primitivas Geométricas 3D Espacio del Modelo Transformaciones del Modelo Espacio del Ojo Iluminación Transformaciones de Vista Espacio de Restricción Restricciones (Clipping) Espacio de la Pantalla Transformaciones de Proyecciones Conversión de Barrido Imagen Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! Primitivas Geométricas 3D con atributos Transformaciones del Modelo Conversión de Barrido Aplicación de Texturas Iluminación Suma de Colores Coordenadas y Transformaciones de Textura Restricciones (Clipping) Transformaciones de Proyecciones Édgar Garduño Ángeles! Niebla AntiAliasing Operaciones sobre Píxeles Imagen C. Computación, I.I.M.A.S! Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! • geometry shaders desde OpenGL 3.2 • tessellation shaders desde OpenGL 4.0 • compute shaders desde OpenGL 4.3. Primitivas Geométricas 3D con atributos Transformaciones de Vértices (Vertex Shaders) Transformaciones de Geometria (Geometry Shaders) Ensamble de Primitivas Ensamble de Primitivas Control de Teselación (Tesselation Control Shaders) Generador de Primitivas de Teselación Evaluación de Teselación (Tesselation Evaluation Shaders) Édgar Garduño Ángeles! Operaciones sobre Píxeles (Fragment Shaders) AntiAliasing Restricciones (Clipping) Operaciones sobre Píxeles Rasterizado Imagen C. Computación, I.I.M.A.S! GLSL Version Versión OpenGL Fecha Shader Preprocessor 1.10.59 2.0 Abril 2004 #versión 110 1.20.8 2.1 Septiembre 2006 #versión 120 1.30.10 3.0 Agosto 2008 #versión 130 1.40.08 3.1 Marzo 2009 #versión 140 1.50.11 3.2 Agosto 2009 #versión 150 3.30.6 3.3 Febrero 2010 #versión 330 4.00.9 4.0 Marzo 2010 #versión 400 4.10.6 4.1 Julio 2010 #versión 410 4.20.11 4.2 Agosto 2011 #versión 420 4.30.8 4.3 Agosto 2012 #versión 430 4.40 4.4 Julio 2013 #versión 440 4.50 4.5 Agosto 2014 #versión 450 Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! ■ ■ ■ La programación de vértices, geometría y fragmentos en OpenGL se conoce como Shading y el lenguaje se conoce OpenGL Shading Language o GLSL. GLSL es un lenguaje de alto nivel que está basado en C/C++ y es el que se utiliza para escribir shaders. Referencias: ● M. Bailey and S. Cunningham, Graphics Shaders Theory an Practice. A K Peters, 2009. ● R. J. Rost, OpenGL(R) Shading Language, (2nd Edition). Addison-Wesley Professional, January 2006. Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! ■ ■ ■ Cuando se utilizan programas de GLSL es necesario crear por lo menos un Vertex Shader y un Fragment Shader. El vertex shader únicamente procesa primitivas de vértices en un esquema entrada/salida estricto: un vértice entra y sale un vértice. En esta etapa el procesamiento se lleva a cabo en forma completamente independiente; a través de la pipeline un vértice no tiene información acerca de los otros lo cual permite procesar varios vértices en paralelo. Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! ■ ■ ■ Cuando se puede utilizar un geometry shader, este procesa primitivas formadas por vértices conectados, tales como líneas o polígonos. En esta etapa toda la información perteneciente a la primitiva se encuentra disponible. En este caso, el shader puede producir primitivas de salida que sean diferentes a las de entrada; sin embargo, los tipos de primitivas de entrada y/o de salida y el número máximo de vértices de salida deben estar predefinidos por el usuario. Cada primitiva de entrada puede generar como salida desde cero primitivas hasta el número máximo, predefinido, de primitivas. Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! ■ ■ ■ En el caso de un fragment shader, este se ejecuta una vez por cada fragmento generado por cada primitiva. Cada ejecución no posee información sobre a cual primitiva pertenece ni acerca de otros fragmentos. Sin embargo, recibe los atributos interpolados de los vértices de la primitiva a la que el fragmento pertenece. La tarea más importante de un fragment shader es producir o descartar un color por cada fragmento. Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! varying vec3 C; void main(void) { // Transformación Proyección ModelView vec4 clipCoord = gl_ModelViewProjectionMatrix * gl_Vertex; gl_Position = clipCoord; // Copia el color primario gl_FrontColor = gl_Color; // Calcula las Coordenadas Normalizadas del Dispositivo vec3 ndc = clipCoord.xyz / clipCoord.w; // Mapea del rango [-1,1] a [0,1] antes de producir una salida C = (ndc * 0.5) + 0.5; } Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! varying vec3 C; void main(void) { // Mezcla los colores primarios y secundarios usando una proporción 50:50 gl_FragColor = mix(gl_Color, vec4(vec3(C), 1.0), 0.5); } Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! ■ Estos son los tipos básicos en GLSL: ● float, float vec2, vec3, vec4 ● int, int ivec2, ivec3, ivec4 ● bool, bool bvec2, bvec3, bvec4 ● void ● matcxr, donde c y r =2,3 o 4 (float) ● sampler1D, sampler2D, sampler3D ● samplerCube ● sampler1DShadow, sampler2DShadow Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! ■ A partir de GLSL 1.30 ● uint ● double Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! struct surface{ float indexOfRefraction; float reflectivity; vec3 color; float turbulence; } myStruct; … surface anotherSurf; Para estructuras del mismo tipo se pueden usar comparaciones =, == y !=. Como en C/C++, se puede acceder a un miembro de la estructura usando el operador ., por ejemplo: float reflection = anotherSurf.reflectivity * 0.6; Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! Se pueden declarar arreglos unidimensionales de cualquier tipo, excepto arreglos de arreglos: surface mySurfaces[]; vec4 lightPositions[8]; vec4 moreLightPositions[] = lightPositions; const int numSurfaces = 5; surface myFiveSurfaces[numSurfaces]; float[5] values;ç Todos los arreglos unidimensionales pueden utilizar el método length para saber sus tamaño: values.length(); Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! const valor constante asignado durante la declaración. attribute datos de vértices de solo lectura. Solo disponibles para vertex shaders. Se declara como global (fuera de todas las funciones). Se puede asignar a float, vectores de reales o matrices de reales pero no a estructuras ni a arreglos. uniform se utiliza para dejar valores fijos durante la ejecución de un shader. A diferencia de un tipo const, un valor uniform no se conoce el valor al momento de compilación y se inicializa fuera del shader. varying es la salida de un vertex shader que corresponde a la entrada interpolada (de solo lectura) a un fragment shader. centroid varying es equivalente a varying cuando no se multi-muestrea, cuando se multi-muestrea, el tipo se evalúa dentro de una posición que cae en el interior de la primitiva que se está rasterizando en lugar de una posición fija, tal como el centro. invariant se utiliza para las salidas de un vertex shader (y entrada del correspondiente fragment shader) para indicar que los valores deben ser consistentes entre varios shaders. NO se debe usar a menos que sea indispensable. in califica un argumento como de solo entrada. Este es el comportamiento por defecto. out califica un argumento como de solo salida. No necesita tener un valor a la entrada. inout califica un argumento como de entrada/salida. El argumento puede tener valores de entrada y regresar valores de salida. Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! Nombre Tipo Descripción gl_Color vec4 Input attribute corresponding to per-vertex primary color. gl_SecondaryColor vec4 Input attribute corresponding to per-vertex secondary color. gl_Normal vec3 Input attribute corresponding to per-vertex normal. gl_Vertex vec4 Input attribute corresponding to object-space vertex position. gl_MultiTexCoordn vec4 Input attribute corresponding to per-vertex texture coordinate n. gl_FogCoord float Input attribute corresponding to per-vertex fog coordinate. gl_Position vec4 Output for transformed vertex position that will be used by fixed functionality primitive assembly, clipping, and culling; all vertex shaders must write to this variable. gl_ClipVertex vec4 Output for the coordinate to use for user clip plane clipping. gl_PointSize float Output for the size of the point to be rasterized, measured in pixels. gl_FrontColor vec4 Varying output for front primary color. gl_BackColor vec4 Varying output for back primary color. gl_FrontSecondaryColor vec4 Varying output for front secondary color. gl_BackSecondaryColor vec4 Varying output for back secondary color. gl_TexCoord[] vec4 Array of varying outputs for texture coordinates. gl_FogFragCoord float Varying output for the fog coordinate. Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! Nombre Tipo Descripción gl_Color vec4 Interpolated read-only input containing the primary color. gl_SecondaryColor vec4 Interpolated read-only input containing the secondary color. gl_TexCoord[] vec3 Array of interpolated read-only inputs containing texture coordinates. gl_FogFragCoord float Interpolated read-only input containing the fog coordinate. gl_FragCoord vec4 Read-only input containing the window-space x, y, z, and 1/w. gl_FrontFacing bool Read-only input whose value is true if part of a front-facing primitive. gl_PointCoord vec2 Two-dimensional coordinates ranging from (0.0, 0.0) to (1.0, 1.0) across a point sprite, defined only for point primitives and when GL_POINT_SPRITE is enabled. gl_FragColor vec4 Output for the color to use for subsequent per-pixel operations. gl_FragData[] vec4 Array of arbitrary data output to be used with glDrawBuffers and cannot to be used in combination with gl_FragColor. gl_FragDepth float Output for the depth to use for subsequent per-pixel operations; if unwritten, the fixed functionality depth is used instead. C. Computación, I.I.M.A.S! Édgar Garduño Ángeles! () [] . ++ -+-! */ < > <= >= == != && || ^^ ?: = += -= *= /= , Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! vec3 myNormal = vec3(0.0, 1.0, 0.0); greenTint = myColor + vec3(0.0, 1.0, 0.0); ivec4 myColor = ivec4(255); vec4 myVector1 = vec4(x, vec2(y, z), w); vec2 myVector2 = vec2(myVector1); // z y w se eliminan float myFloat = float(myVector2); // y se elimina // Todos estos son lo mismo que la matriz identidad 2x2 mat2 myMatrix1 = mat2(1.0, 0.0, 0.0, 1.0); mat2 myMatrix2 = mat2(vec2(1.0, 0.0), vec2(0.0, 1.0)); mat2 myMatrix3 = mat2(1.0); mat2 myMatrix4 = mat2(mat4(1.0)); // takes upper 2x2 of the 4x4 float myFloat = 4.7; int myInt = int(myFloat); // myInt = 4 ivec2 cursorPositions[3] = ivec2[3]((0, 0), (10, 20), (15, 40)); ivec2 morePositions[3] = ivec2[]((0, 0), (10, 20), (15, 40)); struct surface { float indexOfRefraction; float reflectivity; vec3 color; float turbulence; }; surface mySurf = surface(ior, refl, vec3(red, green, blue), turb); Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! ■ ■ ■ ■ ■ ■ Se puede acceder a los componentes individuales de un vector usando la notación de punto junto con {x,y,z,w}, {r,g,b,a}, or {s,t,p,q}. vec3 myVector = {0.25, 0.5, 0.75}; float myR = myVector.r; // 0.25 vec2 myYZ = myVector.yz; // 0.5, 0.75 float myQ = myVector.q; // ILEGAL!! Accede a componentes mas allá de vec3 float myRY = myVector.ry; // ILEGAL!! Combina dos notaciones vec3 myZYX = myVector.zyx; // orden inverse vec4 mySSTT = myVector.sstt; // replica s y t dos veces cada uno vec4 myColor = vec4(0.0, 1.0, 2.0, 3.0); myColor.x = -1.0; // -1.0, 1.0, 2.0, 3.0 myColor.yz = vec2(-2.0, -3.0); // -1.0, -2.0, -3.0, 3.0 myColor.wx = vec2(0.0, 1.0); // 1.0, -2.0, -3.0, 0.0 myColor.zz = vec2(2.0, 3.0); // ILEGAL!! float myY = myVector[1]; // Comportamiento indefinido mat3 myMatrix = mat3(1.0); vec3 myFirstColumn = myMatrix[0]; // primera columna: 1.0, 0.0, 0.0 float element21 = myMatrix[2][1]; // última columna, hilera de enmedio: 0.0 Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! ■ ■ ■ for (l = 0; l < numLights; l++){ if (!lightExists[l]) continue; color += light[l]; } while (lightNum >= 0){ color += light[lightNum]; lightNum—; } do { color += light[lightNum]; lightNum—; } while (lightNum > 0); Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! ■ ■ ■ if (numLights > 0){ color = litColor; } if (numLights > 0){ color = litColor; } else{ color = unlitColor; } Para fragment shaders existe una forma de terminar la ejecución sin procesar el resto de las instrucciones y sin escribir en el framebuffer: if (color.a < 0.9) discard; Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! // declaración bool isAnyComponentNegative(const vec4 v); // Utilización void main() { … bool someNeg = isAnyComponentNegative(gl_MultiTexCoord0); … } // definición bool isAnyComponentNegative(const vec4 v) { … return true; … return false; } Igual que en C++, se permite sobrecarga (overload) de funciones. Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! ■ ■ El procesador de vértices es responsable de ejecutar los programas vertex shaders. Un vertex shader recibe como entrada los datos de vértices (por ejemplo, su posición, color, o normales asociadas) dependiendo de lo que la aplicación de OpenGL envíe. Un vertex shader puede realizar tareas como las siguientes: ● Transformaciones de las posiciones de los vértices usando las ● ● ● ● matrices de modelview y proyección. Transformaciones de normales y su normalización, en caso de ser necesario. Generación y transformación de coordenadas de textura. Iluminación por vértice o cálculo de valores para iluminación por píxel. Cálculo de color. Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! ■ ■ ■ ■ No es necesario realizar todas las operaciones mencionadas en un solo vertex shader, pero hay que tener en cuenta que cuando se usa un vertex shader se remplaza toda la funcionalidad del procesador de vértices; por lo tanto, no se puede esperar que la funcionalidad fija de OpenGL realice las operaciones que no se programaron en el vertex shader: Cuando se usa un vertex shader, éste se hace responsable por realizar toda la funcionalidad en este punto de la línea de producción gráfica. El procesador de vértices no posee información acerca de la conectividad y por ello no se pueden realizar operaciones que requieren conocimiento topológico. Por ejemplo, no se pueden realizar operaciones relacionadas a caras/polígonos. El procesador de vértices procesa vértices individualmente y no tiene idea sobre los otros vértices. Un vertex shader necesita escribir por lo menos una variable: gl_Position, la cual normalmente con el resultado de la transformación del vértice con las matrices de modelview y proyección. Un procesador e vértices tiene acceso al estado de OpenGL, por lo que puede realizar operaciones que involucran iluminación y materiales. También puede acceder a texturas. No se tiene acceso al framebuffer. Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! ■ Los fragment shaders se ejecutan en el procesador de fragmentos, una unidad responsable por operaciones como: ● ● ● ● ■ ■ ■ ■ Cálculo de colores y coordenadas por píxel. Aplicación de texturas. Cálculo de neblina. Cálculo de normales para iluminación por píxel. Un fragment shader recibe como entrada los valores interpolados que han sido calculados en una etapa anterior de la línea de producción grafica tales como las posiciones de los vértices, colores o normales. En un vertex shader estos valores se calculan por vértice, en un fragment shader se trabaja con fragmentos dentro de las primitivas, por lo que se necesitan los valores interpolados. Al igual que en el caso de un vertex shader, un fragment shader substituye la funcionalidad fija de OpenGL y en caso de utilizar un shader no se puede esperar que OpenGL realice las operaciones que no se llevaron a cabo en el shader. Un procesador de fragmentos opera sobre fragmentos individualmente y no tiene idea sobre el resto de fragmentos. El fragment shader también tiene acceso al estado de OpenGL y por lo tanto puede acceder a colores y materiales. Édgar Garduño Ángeles! C. Computación, I.I.M.A.S! ■ ■ Un fragment shader NO puede cambiar la coordenada del píxel que ya fue calculado previamente en la línea de producción. Un fragment shader tiene acceso a las posiciones de los píxeles sobre la pantalla pero no puede cambiarlas. Un fragment shader puede producir una de dos opciones: ● Descartar el fragmento, por lo que no se produce algo/nada. ● Calcular gl_FragColor (el color final del fragmento) o gl_FragData (cuando se hace rendering a varios objetivos). ■ ■ La profundidad tambien puede ser modificada aunque típicamente no es necesario ya que ha sido calculada “adecuadamente” en etapas previas. Un fragment shader no tiene acceso al framebuffer. Por lo tanto, operaciones de mezcla de colores ocurren después de la ejecución del fragment shader. Édgar Garduño Ángeles! C. Computación, I.I.M.A.S!