Difference between revisions of "Shaders in Oolite"

From Elite Wiki
(Uniform reference)
(Updated for 1.68, and updated sample code based on simpler new version of Freaky Thargoids.)
Line 1: Line 1:
 
'''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 '''[http://en.wikipedia.org/wiki/GLSL OpenGL Shading Language]''', also known as '''GLslang''' or '''GLSL'''.
 
'''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 '''[http://en.wikipedia.org/wiki/GLSL 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.
+
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 version 1.68 for Windows will.
  
  
Line 13: Line 13:
 
| ''textures'' || array of strings || A list of textures used by the shader.
 
| ''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.)
+
| ''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. (Requires Oolite 1.68 or later.)
 
|-  
 
|-  
 
| ''glsl-vertex'' || string || GLslang code to use as a vertex shader. This is ignored if ''vertex_shader'' is specified.
 
| ''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.)
+
| ''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. (Requires Oolite 1.68 or later.)
 
|-  
 
|-  
 
| ''glsl-fragment'' || string || GLslang code to use as a fragment shader. This is ignored if ''fragment_shader'' is specified.
 
| ''glsl-fragment'' || string || GLslang code to use as a fragment shader. This is ignored if ''fragment_shader'' is specified.
Line 31: Line 31:
 
This vertex shader, like many vertex shaders, exists primarily to prepare information for the fragment shader.
 
This vertex shader, like many vertex shaders, exists primarily to prepare information for the fragment shader.
 
<pre>varying vec3            v_normal;
 
<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()
 
void main()
 
{
 
{
 
     v_normal = normalize(gl_NormalMatrix * gl_Normal);
 
     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_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;
 
     gl_Position = ftransform();
 
     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>
+
</pre>
 +
The first line declares a '''varying variable'''. 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">
 
[[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.
+
This is followed by the function <code>main()</code>, which is the function that will be called by the GPU for each vertex. This sets the varying normal (direction facing out from the surface), and performs additional set-up required for positions and texture co-ordinates to make sense in the fragment shader.
  
 
=== 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.)
 
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.
+
<pre>// Information from Oolite.
    This shader performs per-vertex lighting (with two lights) and
+
uniform sampler2D   tex0;
    glow mapping. The glow map varies over time, with the alpha
+
uniform sampler2D   tex1;
    channel specifying the phase of the glow effect.
+
uniform float       time;
*/
 
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
+
// Information from shipdata.plist.
varying vec4            v_diffuse0;
+
uniform float      reciprocalFrequency;
varying vec3            v_direction0;
 
  
// Light 1 information
+
// Information from vertex shader.
varying vec4            v_diffuse1;
+
varying vec3       v_normal;
varying vec3           v_direction1;
 
  
  
 
float wave(float t)
 
float wave(float t)
 
{
 
{
     // approximates a sine waveform by summing 4 triangular waveforms
+
     return sin(t * 6.28318530718) * 0.5 + 0.25;
    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;
+
#define LIGHT(idx) \
}
+
     { \
 +
        vec3 lightVector = normalize(gl_LightSource[idx].position.xyz); \
 +
        color += gl_FrontMaterial.diffuse * gl_LightSource[idx].diffuse * max(dot(v_normal, lightVector), 0.0); \
 +
    }
  
  
 
void main(void)
 
void main(void)
 
{
 
{
    vec4 color = v_ambient;
+
     // Calculate illumination.
    vec3 normal = normalize(v_normal);
+
     vec4 color = gl_FrontMaterial.ambient * gl_LightSource[0].ambient;
   
+
//  LIGHT(0);
     // Calculate contribution from two lights
+
     LIGHT(1);
    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
 
     // Load texture data
Line 137: Line 87:
 
      
 
      
 
     // Calculate glow intensity
 
     // Calculate glow intensity
     float t1 = RECIPROCAL_FREQUENCY * time + glowMap.a;
+
     float t1 = reciprocalFrequency * time + glowMap.a;
 
     float lightLevel = wave(t1);
 
     float lightLevel = wave(t1);
 
      
 
      
Line 143: Line 93:
 
     color += lightLevel * glowMap;
 
     color += lightLevel * glowMap;
 
      
 
      
     gl_FragColor = color;
+
     gl_FragColor = vec4(color.rgb, 1.0);
}</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. Uniform variables can only be read, not written to.
+
The first section declares several ''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 <code>timer</code> is a number which increases by 1.0 each second. The <code>float</code> <code>reciprocalFrequency</code> is set in <code>shipData.plist</code>; this allows a shader to be used for different ships (or different shaders on the same ship) with small variations. 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> declaration is the same as in the vertex shader, but fragment shaders can only read varying variables, not write to them.
  
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 <code>wave()</code> is used to smoothly vary the glow map from on to off.
  
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.
+
This is followed by a macro definition, <code>LIGHT</code>. A macro is substituted into the code each time it occurs; in <code>main()</code>, <code>LIGHT(0)</code> and <code>LIGHT(1)</code> will be replaced by the text of the macro definition, with <code>idx</code> in the macro being replaced with 0 or 1 respectively. Ideally, this would be a function, like <code>wave()</code>, but for technical reasons this causes performance problems. Specifically, under Apple’s implementation of GLSL (and probably others), this causes a set of uniforms for every light the graphics card supports to be pulled into the shader.
  
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.
+
The last part is the function <code>main()</code>, which is the function executed by the GPU. It first calculates the contribution of light source 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. Ideally, the shader would check light 0, but in current versions of Oolite this causes problems.) 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, the <code>reciprocalFrequency</code> uniform 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. Finally, the combined alpha channel is forced to 1.0, and the result assigned to the special variable <code>gl_FragColor</code>, which determines the colour of the generated fragment.
  
  
Line 173: Line 124:
 
| ''engine_level'' || <code>float</code> || Engine thrust. 0.0 for no movement, 1.0 for full thrust. Greater than 1.0 for injectors or hyperspeed.
 
| ''engine_level'' || <code>float</code> || Engine thrust. 0.0 for no movement, 1.0 for full thrust. Greater than 1.0 for injectors or hyperspeed.
 
|-
 
|-
| ''entity_personality'' || <code>float</code> || A randomly-generated value in the range 0.0 to 1.0 that stays with the entity for its lifetime. Useful for adding random variations. (Oolite 1.68 and later)
+
| ''entity_personality'' || <code>float</code> || A randomly-generated value in the range 0.0 to 1.0 that stays with the entity for its lifetime. Useful for adding random variations. (Requires Oolite 1.68 or later.)
 
|-
 
|-
| ''entity_personality_int'' || <code>int</code> || Same as ''entity_personality'', scaled to the range 0 to 32767. (Oolite 1.68 and later)
+
| ''entity_personality_int'' || <code>int</code> || Same as ''entity_personality'', scaled to the range 0 to 32767. (Requires Oolite 1.68 or later.)
 
|-
 
|-
| ''hull_heat_level'' || <code>float</code> || Hull temperature. 1.0 is damage level. (Oolite 1.68 and later)
+
| ''hull_heat_level'' || <code>float</code> || Hull temperature. 1.0 is damage level. (Requires Oolite 1.68 or later.)
 
|-
 
|-
 
| ''laser_heat_level'' || <code>float</code> || Laser temperature, ranging from 0.0 to 1.0.
 
| ''laser_heat_level'' || <code>float</code> || Laser temperature, ranging from 0.0 to 1.0.
Line 185: Line 136:
  
  
In order to enable shaders to use new features while remaining backwards compatible, each available uniform (except tex'''N''') has an associated macro. For instance, to test whether ''hull_heat_level'' is available, use:
+
In order to enable shaders to use new features while remaining backwards compatible, each available uniform (except tex'''N''') has an associated macro (starting with Oolite 1.68). For instance, to test whether ''hull_heat_level'' is available, use:
 
<pre>#ifdef OO_HULL_HEAT_LEVEL
 
<pre>#ifdef OO_HULL_HEAT_LEVEL
 
uniform float hull_heat_level;
 
uniform float hull_heat_level;

Revision as of 23:36, 4 April 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 version 1.68 for Windows will.


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:

shaders dictionary values
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. (Requires Oolite 1.68 or later.)
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. (Requires Oolite 1.68 or later.)
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;

void main()
{
    v_normal = normalize(gl_NormalMatrix * gl_Normal);
    
    gl_TexCoord[0] = gl_TextureMatrix[0] * gl_MultiTexCoord0;
    gl_Position = ftransform();
}

The first line declares a varying variable. 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:
Glsl varying demo.png
This is followed by the function main(), which is the function that will be called by the GPU for each vertex. This sets the varying normal (direction facing out from the surface), and performs 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.)

// Information from Oolite.
uniform sampler2D   tex0;
uniform sampler2D   tex1;
uniform float       time;

// Information from shipdata.plist.
uniform float       reciprocalFrequency;

// Information from vertex shader.
varying vec3        v_normal;


float wave(float t)
{
    return sin(t * 6.28318530718) * 0.5 + 0.25;
}


#define LIGHT(idx) \
    { \
        vec3 lightVector = normalize(gl_LightSource[idx].position.xyz); \
        color += gl_FrontMaterial.diffuse * gl_LightSource[idx].diffuse * max(dot(v_normal, lightVector), 0.0); \
    }


void main(void)
{
    // Calculate illumination.
    vec4 color = gl_FrontMaterial.ambient * gl_LightSource[0].ambient;
//  LIGHT(0);
    LIGHT(1);
    
    // 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 = reciprocalFrequency * time + glowMap.a;
    float lightLevel = wave(t1);
    
    // Add glow.
    color += lightLevel * glowMap;
    
    gl_FragColor = vec4(color.rgb, 1.0);
}

The first section declares several 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. The float reciprocalFrequency is set in shipData.plist; this allows a shader to be used for different ships (or different shaders on the same ship) with small variations. Uniform variables can only be read, not written to.

The varying declaration is the same as in the vertex shader, but fragment shaders can only read varying variables, not write to them.

The custom function wave() is used to smoothly vary the glow map from on to off.

This is followed by a macro definition, LIGHT. A macro is substituted into the code each time it occurs; in main(), LIGHT(0) and LIGHT(1) will be replaced by the text of the macro definition, with idx in the macro being replaced with 0 or 1 respectively. Ideally, this would be a function, like wave(), but for technical reasons this causes performance problems. Specifically, under Apple’s implementation of GLSL (and probably others), this causes a set of uniforms for every light the graphics card supports to be pulled into the shader.

The last part is the function main(), which is the function executed by the GPU. It first calculates the contribution of light source 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. Ideally, the shader would check light 0, but in current versions of Oolite this causes problems.) 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, the reciprocalFrequency uniform 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. Finally, the combined alpha channel is forced to 1.0, and the result assigned to the special variable gl_FragColor, which determines the colour of the generated fragment.


Uniform reference

The uniform variables currently provided by Oolite are:

Uniform variables
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.
engine_level float Engine thrust. 0.0 for no movement, 1.0 for full thrust. Greater than 1.0 for injectors or hyperspeed.
entity_personality float A randomly-generated value in the range 0.0 to 1.0 that stays with the entity for its lifetime. Useful for adding random variations. (Requires Oolite 1.68 or later.)
entity_personality_int int Same as entity_personality, scaled to the range 0 to 32767. (Requires Oolite 1.68 or later.)
hull_heat_level float Hull temperature. 1.0 is damage level. (Requires Oolite 1.68 or later.)
laser_heat_level float Laser temperature, ranging from 0.0 to 1.0.
time float Uniformly increasing timer, in seconds.


In order to enable shaders to use new features while remaining backwards compatible, each available uniform (except texN) has an associated macro (starting with Oolite 1.68). For instance, to test whether hull_heat_level is available, use:

#ifdef OO_HULL_HEAT_LEVEL
uniform float hull_heat_level;
#endif

Limitations

The current implementation does not provide the information necessary to implement the most common form of normal mapping.