Difference between revisions of "Shaders in Oolite"
m (Fragment shaders in Oolite moved to Shaders in Oolite: Generalized - contrary to my belief, Oolite already supported vertex shaders.) |
|||
Line 28: | Line 28: | ||
A full explanation of GLslang is beyond the scope of this article, but for illustrative purposes, here is an overview of the fragment shader code in the [[Freaky Thargoids OXP|Freaky Thargoids]] example OXP. |
A full explanation of GLslang is beyond the scope of this article, but for illustrative purposes, here is an overview of the fragment shader code in the [[Freaky Thargoids OXP|Freaky Thargoids]] example OXP. |
||
− | The shader code, which is specified in ''shipdata.plist'', looks like this: |
||
+ | === Vertex shader === |
||
− | <pre>uniform sampler2D tex0; |
||
+ | This vertex shader, like many vertex shaders, exists primarily to prepare information for the fragment shader. |
||
+ | <pre>varying vec3 v_normal; |
||
+ | varying vec4 v_ambient; |
||
+ | |||
+ | // Light 0 information |
||
+ | varying vec4 v_diffuse0; |
||
+ | varying vec3 v_direction0; |
||
+ | |||
+ | // Light 1 information |
||
+ | varying vec4 v_diffuse1; |
||
+ | varying vec3 v_direction1; |
||
+ | |||
+ | void main() |
||
+ | { |
||
+ | v_normal = normalize(gl_NormalMatrix * gl_Normal); |
||
+ | |||
+ | v_ambient = gl_LightModel.ambient * gl_FrontMaterial.ambient |
||
+ | + gl_FrontMaterial.ambient * gl_LightSource[0].ambient |
||
+ | + gl_FrontMaterial.ambient * gl_LightSource[1].ambient; |
||
+ | |||
+ | // Set up light 0 information |
||
+ | v_direction0 = normalize(gl_LightSource[0].position.xyz); |
||
+ | v_diffuse0 = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse; |
||
+ | |||
+ | // Set up light 1 information |
||
+ | v_direction1 = normalize(gl_LightSource[1].position.xyz); |
||
+ | v_diffuse1 = gl_FrontMaterial.diffuse * gl_LightSource[1].diffuse; |
||
+ | |||
+ | gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0; |
||
+ | gl_Position = ftransform(); |
||
+ | }</pre> |
||
+ | The first section declares several '''varying variables'''. This may sound like an oxymoron, but varying variables have the special property that they are interpolated across geometry and passed to the fragment shader. For instance, if you set a varying variable to red at one corner of a triangle, green at a second corner, and blue at the third, the colour as seen by the fragment shader will vary smoothly across the triangle:<br> |
||
+ | [[Image:Glsl varying demo.png]]<br clear="all"> |
||
+ | These declarations are followed by the function ''main()'', which is the function that will be called by the GPU for each vertex. This prepares state required to perform diffuse [http://en.wikipedia.org/wiki/Gouraud_shading Gouraud shading] with two lights, and additional set-up required for positions and texture co-ordinates to make sense in the fragment shader. |
||
+ | |||
+ | === Fragment shader === |
||
+ | A fragment shader is called for each fragment of a generated polygon. A '''fragment''' is a pixel, in screen space, on which the polygon is potentially visible. (It is ''potentially'' visible because the shader may discard the fragment, and later polygons which are closer to the camera may draw over it.) |
||
+ | <pre>/* Freaky Thargoid: shader example for Oolite. |
||
+ | This shader performs per-vertex lighting (with two lights) and |
||
+ | glow mapping. The glow map varies over time, with the alpha |
||
+ | channel specifying the phase of the glow effect. |
||
+ | */ |
||
+ | uniform sampler2D tex0; |
||
uniform sampler2D tex1; |
uniform sampler2D tex1; |
||
− | |||
+ | |||
uniform float time; |
uniform float time; |
||
+ | |||
+ | |||
+ | #define RECIPROCAL_FREQUENCY 1.5 |
||
+ | |||
+ | |||
+ | varying vec3 v_normal; |
||
+ | varying vec4 v_ambient; |
||
+ | |||
+ | // Light 0 information |
||
+ | varying vec4 v_diffuse0; |
||
+ | varying vec3 v_direction0; |
||
+ | |||
+ | // Light 1 information |
||
+ | varying vec4 v_diffuse1; |
||
+ | varying vec3 v_direction1; |
||
+ | |||
float wave(float t) |
float wave(float t) |
||
{ |
{ |
||
− | / |
+ | // approximates a sine waveform by summing 4 triangular waveforms |
− | |||
float s0 = t; |
float s0 = t; |
||
s0 -= floor(s0); |
s0 -= floor(s0); |
||
Line 56: | Line 114: | ||
} |
} |
||
− | void main() |
||
− | { |
||
− | float t1 = 1.5 * time + texture2D(tex1, gl_TexCoord[0].st).a; |
||
− | float effect = wave(t1); |
||
+ | void main(void) |
||
− | |||
+ | { |
||
− | vec4 base = texture2D(tex0, gl_TexCoord[0].st); |
||
+ | vec4 color = v_ambient; |
||
− | vec4 glow = texture2D(tex1, gl_TexCoord[0].st); |
||
+ | vec3 normal = normalize(v_normal); |
||
− | gl_FragColor = gl_Color * base + effect * glow; |
||
+ | // Calculate contribution from two lights |
||
+ | float NdotL; |
||
+ | NdotL = max(dot(normal, v_direction0), 0.0); |
||
+ | color += NdotL * v_diffuse0; |
||
+ | |||
+ | NdotL = max(dot(normal, v_direction1), 0.0); |
||
+ | color += NdotL * v_diffuse1; |
||
+ | |||
+ | // Load texture data |
||
+ | vec2 texCoord = gl_TexCoord[0].st; |
||
+ | vec4 colorMap = texture2D(tex0, texCoord); |
||
+ | vec4 glowMap = texture2D(tex1, texCoord); |
||
+ | |||
+ | // Multiply illumination by base colour |
||
+ | color *= colorMap; |
||
+ | |||
+ | // Calculate glow intensity |
||
+ | float t1 = RECIPROCAL_FREQUENCY * time + glowMap.a; |
||
+ | float lightLevel = wave(t1); |
||
+ | |||
+ | // Add glow. |
||
+ | color += lightLevel * glowMap; |
||
+ | |||
+ | gl_FragColor = color; |
||
}</pre> |
}</pre> |
||
− | The first section declares three ''uniform variables'', which are used to pass information from Oolite to the shader. The two <code>sampler2D</code> variables are used to read from the two textures used by the shader. The <code>float</code> variable ''timer'' is a number which increases by 1.0 each second. |
+ | The first section declares three ''uniform variables'', which are used to pass information from Oolite to the shader. The two <code>sampler2D</code> variables are used to read from the two textures used by the shader. The <code>float</code> variable ''timer'' is a number which increases by 1.0 each second. Uniform variables can only be read, not written to. |
+ | |||
+ | The <code>#define</code> line defines a ''macro'', which is a value substituted into the code. In this case, wherever <code>RECIPROCAL_FREQUENCY</code> is encountered, it will be replaced with the value 1.5. This allows “tweak factors” to be grouped together at the top of a file for easy identification and editing. (The value of <code>RECIPROCAL_FREQUENCY</code> is the only difference between the thargoid and tharglet shaders in Freaky Thargoids. Ideally, Oolite would provide a way to specify uniform variables in ''shipdata.plist'' so that the same shader could be used, with minor tweaks, on different models, but this is not currently the case.) |
||
+ | |||
+ | The <code>varying</code> declarations are the same as in the vertex shader, but fragment shaders can only read varying variables, not write to them. |
||
− | + | The custom function ''wave()'' is an approximation of the trigonometric [http://en.wikipedia.org/wiki/Sine sine function]]. It is used to smoothly vary the glow map from on to off. |
|
− | The last part is the function ''main()'', which is the function executed by the GPU. It first calculates |
+ | The last part is the function ''main()'', which is the function executed by the GPU. It first calculates the contribution of light sources 0 and 1, using the information prepared in the vertex shader and interpolated by the GPU. (Light 0 is used for the demo screen and shipyard; light 1 is the sun, or an arbitrary light source when in witchspace. Future versions of Oolite may change this, though.) It then loads values from the two textures. The first, the colour map value, is simply multiplied by the incoming light. For the second, the glow map, an intensity value is calculated based on the time (multiplying by 1.5 gives a frequency of 1.5 pulsations per second, or 1/1.5 = 0.666… Hz) and the alpha channel of the glow map, which specifies animation phase. The glow map is likewise multiplied by its intensity value, and added to the total light (colour) of the fragment. |
Revision as of 23:00, 22 March 2007
Shaders are programs which run on a graphics processing unit. They provide a more flexible alternative or supplement to textures for specifying objects’ appearance. There are two widely-used types of shader, vertex shaders and fragment shaders (also known, less accurately, as pixel shaders), which are generally used in combination. Shaders can be implemented in a number of special-purpose programming languages; Oolite uses the OpenGL Shading Language, also known as GLslang or GLSL.
Shaders are supported in Oolite 1.67 for Mac OS X and later. At the time of writing, no released version of Oolite for other platforms supports shaders, but the next Windows release will.
Contents
Specifying shaders
Shaders are specified in a dictionary named shaders in the ship’s definition in shipdata.plist. The keys of this dictionary are names of textures used in the ship’s or entity’s .dat file, and the values are dictionaries specifying shaders to use instead. The elements are: The uniform variables currently provided by Oolite are:
Name | Type | Description |
---|---|---|
textures | array of strings |
A list of textures used by the shader. |
vertex_shader | string |
The name of a vertex shader file to use. Oolite will search the Shaders folder of all installed OXPs for a shader of the appropriate name. (Not implemented in 1.67.1 or earlier.) |
glsl-vertex | string |
GLslang code to use as a vertex shader. This is ignored if vertex_shader is specified. |
fragment_shader | string |
The name of a fragment shader file to use. Oolite will search the Shaders folder of all installed OXPs for a shader of the appropriate name. (Not implemented in 1.67.1 or earlier.) |
glsl-fragment | string |
GLslang code to use as a fragment shader. This is ignored if fragment_shader is specified. |
glsl | string |
Synonym for glsl-fragment. |
An example
A full explanation of GLslang is beyond the scope of this article, but for illustrative purposes, here is an overview of the fragment shader code in the Freaky Thargoids example OXP.
Vertex shader
This vertex shader, like many vertex shaders, exists primarily to prepare information for the fragment shader.
varying vec3 v_normal; varying vec4 v_ambient; // Light 0 information varying vec4 v_diffuse0; varying vec3 v_direction0; // Light 1 information varying vec4 v_diffuse1; varying vec3 v_direction1; void main() { v_normal = normalize(gl_NormalMatrix * gl_Normal); v_ambient = gl_LightModel.ambient * gl_FrontMaterial.ambient + gl_FrontMaterial.ambient * gl_LightSource[0].ambient + gl_FrontMaterial.ambient * gl_LightSource[1].ambient; // Set up light 0 information v_direction0 = normalize(gl_LightSource[0].position.xyz); v_diffuse0 = gl_FrontMaterial.diffuse * gl_LightSource[0].diffuse; // Set up light 1 information v_direction1 = normalize(gl_LightSource[1].position.xyz); v_diffuse1 = gl_FrontMaterial.diffuse * gl_LightSource[1].diffuse; gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0; gl_Position = ftransform(); }
The first section declares several varying variables. This may sound like an oxymoron, but varying variables have the special property that they are interpolated across geometry and passed to the fragment shader. For instance, if you set a varying variable to red at one corner of a triangle, green at a second corner, and blue at the third, the colour as seen by the fragment shader will vary smoothly across the triangle:
These declarations are followed by the function main(), which is the function that will be called by the GPU for each vertex. This prepares state required to perform diffuse Gouraud shading with two lights, and additional set-up required for positions and texture co-ordinates to make sense in the fragment shader.
Fragment shader
A fragment shader is called for each fragment of a generated polygon. A fragment is a pixel, in screen space, on which the polygon is potentially visible. (It is potentially visible because the shader may discard the fragment, and later polygons which are closer to the camera may draw over it.)
/* Freaky Thargoid: shader example for Oolite. This shader performs per-vertex lighting (with two lights) and glow mapping. The glow map varies over time, with the alpha channel specifying the phase of the glow effect. */ uniform sampler2D tex0; uniform sampler2D tex1; uniform float time; #define RECIPROCAL_FREQUENCY 1.5 varying vec3 v_normal; varying vec4 v_ambient; // Light 0 information varying vec4 v_diffuse0; varying vec3 v_direction0; // Light 1 information varying vec4 v_diffuse1; varying vec3 v_direction1; float wave(float t) { // approximates a sine waveform by summing 4 triangular waveforms float s0 = t; s0 -= floor(s0); float sum = abs( s0 - 0.5); float s1 = t - 0.125; s1 -= floor(s1); sum += abs( s1 - 0.5) - 0.25; float s2 = t - 0.250; s2 -= floor(s2); sum += abs( s2 - 0.5) - 0.25; float s3 = t - 0.375; s3 -= floor(s3); sum += abs( s3 - 0.5) - 0.25; return sum; } void main(void) { vec4 color = v_ambient; vec3 normal = normalize(v_normal); // Calculate contribution from two lights float NdotL; NdotL = max(dot(normal, v_direction0), 0.0); color += NdotL * v_diffuse0; NdotL = max(dot(normal, v_direction1), 0.0); color += NdotL * v_diffuse1; // Load texture data vec2 texCoord = gl_TexCoord[0].st; vec4 colorMap = texture2D(tex0, texCoord); vec4 glowMap = texture2D(tex1, texCoord); // Multiply illumination by base colour color *= colorMap; // Calculate glow intensity float t1 = RECIPROCAL_FREQUENCY * time + glowMap.a; float lightLevel = wave(t1); // Add glow. color += lightLevel * glowMap; gl_FragColor = color; }
The first section declares three uniform variables, which are used to pass information from Oolite to the shader. The two sampler2D
variables are used to read from the two textures used by the shader. The float
variable timer is a number which increases by 1.0 each second. Uniform variables can only be read, not written to.
The #define
line defines a macro, which is a value substituted into the code. In this case, wherever RECIPROCAL_FREQUENCY
is encountered, it will be replaced with the value 1.5. This allows “tweak factors” to be grouped together at the top of a file for easy identification and editing. (The value of RECIPROCAL_FREQUENCY
is the only difference between the thargoid and tharglet shaders in Freaky Thargoids. Ideally, Oolite would provide a way to specify uniform variables in shipdata.plist so that the same shader could be used, with minor tweaks, on different models, but this is not currently the case.)
The varying
declarations are the same as in the vertex shader, but fragment shaders can only read varying variables, not write to them.
The custom function wave() is an approximation of the trigonometric sine function]. It is used to smoothly vary the glow map from on to off.
The last part is the function main(), which is the function executed by the GPU. It first calculates the contribution of light sources 0 and 1, using the information prepared in the vertex shader and interpolated by the GPU. (Light 0 is used for the demo screen and shipyard; light 1 is the sun, or an arbitrary light source when in witchspace. Future versions of Oolite may change this, though.) It then loads values from the two textures. The first, the colour map value, is simply multiplied by the incoming light. For the second, the glow map, an intensity value is calculated based on the time (multiplying by 1.5 gives a frequency of 1.5 pulsations per second, or 1/1.5 = 0.666… Hz) and the alpha channel of the glow map, which specifies animation phase. The glow map is likewise multiplied by its intensity value, and added to the total light (colour) of the fragment.
Uniform reference
The uniform variables currently provided by Oolite are:
Name | Type | Description |
---|---|---|
tex0 | sampler2D |
Sampler for the first texture. |
tex1 | sampler2D |
Sampler for the second texture. |
… | ||
texN | sampler2D |
Sampler for the N+1th texture. |
time | float |
Uniformly increasing timer, in seconds. |
engine_level | float |
Engine thrust. 0.0 for no movement, 1.0 for full thrust. Greater than 1.0 for injectors or hyperspeed? Untested. |
laser_heat_level | float |
Laser temperature, ranging from 0.0 to 1.0. |
hull_heat_level | float |
Hull temperature. 1.0 is damage level. (Not implemented in 1.67.1 or earlier.) |
Limitations
The current implementation does not provide the information necessary to implement the most common form of normal mapping.