Pixel Shaders in Modern Graphics Rendering There is a good tutorial at How did pixel shaders come about? - Classic OpenGL 1.x pipeline - Assumes known "best practices" for lighting, shadows, texturing - Lighting is Phong illumination (ambient, diffuse, specular) done per vertex - If you use multiple textures, they MUST be added or multiplied together, can't do bump maps!! - OpenGL Extensions alliviate some of these (DOT3 multitexturing for bump maps), but rendering was still very limited - Also, lots of knobs to tweak, learning the API is complicated! - Could have 0, 1, 2, or more textures - Could have up to 8 lights, many different types of lights - Could autogenerate texture coords OR have them specified - Could be blending between matrices OR NOT - IT WAS A PAIN TO LEARN! - Video Cards supported all these knobs by being programmable - The hardware was designed to do general purpose vector math fast, which is what all of this is - PIXEL SHADERS expose this programmability - With Vertex Shaders and Pixel Shaders, you get more flexibility - Enables options for lighting - as fast or realistic as you want! - BDRF (Bi-Directional Radiosity Function) - Cartoon (outlining, hard lighting edges) (no slower than phong!) - Enables simple color processing you couldn't do before - Hue shifts, volumetric fogging - Can move CPU processing to the GPU - We do all of our particle system logic in the vertex shader! - Can do skeletal animation, interpolated animation in th der! - Can calculate pixel coverage in the Pixel Shader! - Can generate tex coords in the vertex shader! - GLSL -- The OpenGL Shading language - Write "C-like" code, can use structures, if, for. - Read from globals that represent input - Write to globals that represent output - Example: a simple pixel shader that writes the color red: void main() { gl_FragColor = vec4( 1.0, 0.0, 0.0, 1.0 ); } - Example, a simple pixel shader that does the Texture + modcolor we currently use in DrawSprite(): uniform sampler2d texture; void main() { gl_FragColor = texture2d( texture, gl_TexCoord ) * gl_Color; } - Example, a hue shift shader uniform sampler2D Texture; uniform vec4 HSVShift; // See Wikipedia page on hue, saturation, value and compare code // with their formulas -- should be straightforward! vec4 rgbaToHsva( vec4 rgba ) { vec4 hsva; float max = max( rgba.r, max( rgba.g, rgba.b )); float min = min( rgba.r, min( rgba.g, rgba.b )); if( max == min ) { hsva.x = 0.0; } else if( rgba.r >= rgba.g && rgba.r >= rgba.b ) { hsva.x = 60.0 * (rgba.g - rgba.b) / (max - min) + 360.0; } else if( rgba.g >= rgba.r && rgba.g >= rgba.b ) { hsva.x = 60.0 * (rgba.b - rgba.r) / (max - min) + 120.0; } else { hsva.x = 60.0 * (rgba.r - rgba.g) / (max - min) + 240.0; } hsva.x = mod( hsva.x, 360.0 ); if( max == 0.0 ) { hsva.y = 0.0; } else { hsva.y = 1.0 - min/max; } hsva.z = max; hsva.a = rgba.a; return hsva; } vec4 hsvaToRgba( vec4 hsva ) { hsva.yz = clamp( hsva.yz, 0.0, 1.0 ); float h_i = mod( floor( hsva.x / 60.0 ), 6.0 ); float f = fract( hsva.x / 60.0 ); float p = hsva.z * (1.0 - hsva.y); float q = hsva.z * (1.0 - f * hsva.y); float t = hsva.z * (1.0 - (1.0 - f) * hsva.y); if( h_i < 0.5 ) { return vec4( hsva.z, t, p, hsva.a ); } else if( h_i < 1.5 ) { return vec4( q, hsva.z, p, hsva.a ); } else if( h_i < 2.5 ) { return vec4( p, hsva.z, t, hsva.a ); } else if( h_i < 3.5 ) { return vec4( p, q, hsva.z, hsva.a ); } else if( h_i < 4.5 ) { return vec4( t, p, hsva.z, hsva.a ); } else { return vec4( hsva.z, p, q, hsva.a ); } } void main() { vec4 color = rgbaToHsva( texture2D( Texture, gl_TexCoord[0] )); color.xyz += HSVShift.xyz; gl_FragColor = hsvaToRgba( color ); } - How to create shaders - Shaders, Programs (a collection of shaders) - Shaders - GLuint glCreateShader(GLenum shaderType) - shaderType must be GL_VERTEX_SHADER or GL_FRAGMENT_SHADER - void glShaderSource(GLuint shader, int numStrings, const char** strings, int* stringLen ) - Usually just call as glShaderSource(s, 1, &shaderText, NULL ) - void glCompileShader(GLuint shader) - Check for errors afterward! - void glGetShaderiv( GLuint shader, GL_COMPILE_STATUS, GLint* result ) - should be GL_TRUE - void glGetShaderInfoLog( GLuint shader, int maxLen, int* actualLen, const char* log); - Get errors - void glDeleteShader( GLuint shader ) - Programs - GLuint glCreateProgram( void ) - void glAttachShader( GLuint program, GLuint shader ) - Do not need to attach vertex AND pixel shaders, you can use fixed-function for part of your code - void glLinkProgram( GLuint program ) - Combines all the programs, like the linker. Lets you split up - Again, check for errors! - void glGetProgramiv( GLunit shader, GL_LINK_STATUS, GLint* result ) - Again, should be GL_TRUE - void glGetProgramInfoLog(GLuint program, int maxLen, int* len, const char* log); - glDeleteProgram( GLuint program ) - How to WRITE shaders - Inputs: - uniform - Constant for an entire model (matrices, hue shift) - attribute - Per-vertex data (position, tex coords) - varying - Output from per-vertex data, gets interpolated (tex coords) - interpolation qualifiers - smooth (default, perspective corrected lerp) - flat (single value, faster) - noperspective (standard lerp) - Even though GLSL is very C like, there are some major differences due to the hardware: - No actual jumps or conditionals in hardware - Both branches of an if-else cost performance! - for loops need a known max iterations - while/do-while loops too! - No pointers! - Une out or inout parameters instead - Different built in types - float, int, bool, vec2, vec3, vec4, mat2, mat3, mat4 - A lot of built in functions - Swizzling - Seems confusing, but it is useful -- trust me here until you start writing actual code - float is the fastest type - Pedantic about float <-> int conversions. - Remember, 0 is an int, 0.0 is a float - If you have an int variable, you can convert it to a float with C++-style constructor syntax float( var ) - How to use Shaders at runtime - void glUseProgram( GLuint program ) - Specify 0 to use the old fixed-function - If in use, things like texturing enabled, lighting enabled, etc. are IGNORED - GLint glGetUniformLocation( GLuint program, const char* name ) - GLint glGetAttribLocation( GLuint program, const char* name ) - Returns a location that's passed to - Can use array subscript and struct access: All these are legal glGetUniformLocation( prog, "Texture" ) glGetUniformLocation( prog, "Texture[0]" ) glGetUniformLocation( prog, "LightData[0].AmbientColor" ) - void glVertexAttrib4f( GLuint loc, GLfloat f0, GLfloat f1, GLfloat f2, GLfloat f3 ) - Use in between call to glBegin(), glEnd() - Very similar in behavior to glColor3ub(), glTexCoord2f - void glUniform4f( GLuint loc, GLfloat f0, GLfloat f1, GLfloat f2, GLfloat f3 ) - Use BEFORE call to glBegin() - Uniforms are once per model