-
Notifications
You must be signed in to change notification settings - Fork 47
Creating Effects
Even simple programs often need to make use of a number of different shader effects. This article explains how to load OpenGL shader files into instances of Ultraviolet's Effect
class using the OpenGL implementation of the Framework.
An Effect
is essentially a hierarchical collection of related shader programs. Each effect has one or more effect techniques, and each technique has one or more effect passes. A technique is just a named collection of of conceptually related passes, and a pass can be thought of as equivalent to a single OpenGL shader program consisting of a vertex shader and a fragment shader. Since only one shader program can be active at a time, using multiple effect passes requires objects to be rendered multiple times in sequence, once for each pass.
Ultraviolet provides some built-in effects, such as BasicEffect
, for simple rendering scenarios. In cases where you need more complete control over the rendering pipeline, however, you'll need to create custom effects which use your own shader code.
In the OpenGL implementation of Ultraviolet's Graphics subsystem, Effect
instances can be represented as either JSON or XML files. Each such file describes the hierarchy of techniques and passes for a particular kind of effect.
All of the formats described below load additional assets which represent the source code for individual vertex and fragment shaders. These assets should use the *.vert
and *.frag
extensions, for vertex shaders and fragment shaders, respectively.
Version 1 XML effect files have the following layout.
<?xml version="1.0" encoding="utf-8" ?>
<Effect>
<Technique Name="TechniqueName">
<Pass Name="PassName">
<VertexShader>VertexShaderAsset</VertexShader>
<FragmentShader>FragmentShaderAsset</FragmentShader>
</Pass>
</Technique>
</Effect>
Multiple <Technique>
elements can be specified under the root node, and multiple <Pass>
elements can be specified for each technique. The values of the <VertexShader>
and <FragmentShader>
elements should be relative asset paths of text files containing the raw GLSL source code for each shader.
Version 2 XML effect files have the following layout.
<?xml version="1.0" encoding="utf-8" ?>
<Effect Version="2">
<Parameters>
<Parameter>parameter1</Parameter>
<Parameter>parameter2</Parameter>
</Parameters>
<Techniques>
<Technique Name="MyTechnique">
<Passes>
<Pass Name="MyPass">
<Stages>
<Vert>VertexShaderAssetPath</Vert>
<VertES>VertexShaderAssetPathES</VertES> <!-- Optional -->
<Frag>FragmentShaderAssetPath</Frag>
<FragES>FragmentShaderAssetPathES</FragES> <!-- Optional -->
</Stages>
</Pass>
</Passes>
</Technique>
</Techniques>
</Effect>
The list of parameters under the <Parameters>
element should correspond to the names of shader uniforms, and will be exposed through the Effect
instance's Parameters
property. Otherwise, the layout of the file is largely the same as V1, if somewhat more verbose.
The <VertES>
and <FragES>
elements can be omitted from the <Stages>
element. If specified, these asset paths will be preferred over those given by <Vert>
and <Frag>
if Ultraviolet is running on OpenGL ES.
JSON effect files have a format which is directly analogous to the V2 XML described above.
{
"parameters": [ "parameter1", "parameter2" ],
"techniques": [
{
"name": "MyTechnique",
"passes": [
{
"name": "MyPass",
"stages": {
"vert": "VertexShaderAssetPath",
"es_vert": "VertexShaderAssetPath",
"frag": "FragmentShaderAssetPath",
"es_frag": "FragmentShaderAssetPath"
}
}
]
}
]
}
As before, the es_vert
and es_frag
fields are optional and may be omitted if not required.
For the simplest scenarios, you don't actually need to create an XML or JSON file at all. If your effect has a single technique with a single pass, you can just load one of the shaders directly:
var effect = content.Load<Effect>("shader.frag");
// or...
var effect = content.Load<Effect>("shader.vert");
So long as the content manager can locate a corresponding vertex/fragment shader with the same name as the shader being loaded, it will automatically construct an effect from that pair of shaders.
You can customize an Effect
instance's behavior at runtime by changing the values of its effect parameters. Each parameter in an effect corresponds to a particular shader uniform, and you can access their values using the Parameters
property of the Effect
class.
effect.Parameters["Foo"].SetValue(bar);
The code above will set any uniforms named Foo
in any of the effect's shader programs to the value bar
. This means that all uniforms with the same name that exist within the same effect must have the same data type, regardless of whether they are ultimately linked into the same shader program.
You specify which shader program to use when rendering by applying an effect pass. Since an effect technique can have more than one pass, this is usually done in a loop, as in the example below.
foreach (var pass in effect.CurrentTechnique.Passes)
{
pass.Apply();
// render the scene
}
At the beginning of each loop, the appropriate shader program is sent to the graphics device and any relevant uniforms are set to the correct values. All subsequent Draw()
calls will be rendered using that shader program, until a new program is set by another call to the Apply()
method.
When Ultraviolet loads a GLSL shader file, it pre-processes the source code prior to compiling it. This allows Ultraviolet to implement a number of useful extensions to the GLSL preprocessor, most of which give Ultraviolet hints about how it should interface with the shader.
To improve compatibility when shaders are shared between Ultraviolet and other engines, all of the directives listed here can be prefixed with //
immediately before the #
symbol, i.e. //#include "foo"
. This will cause them to be interpreted as comments when not loaded by Ultraviolet.
An #include
directive is replaced with the contents of the file found at the specified relative asset path.
As above, but the directive is replaced with the contents of the specified embedded resource file. Specifying entry
or executing
will cause the resource to be loaded from either Assembly.GetEntryAssembly()
or Assembly.GetExecutingAssembly()
, with entry
being the default.
This directive has four variants: #ifver_lt
(less than), #ifver_lte
(less than or equal to), #ifver_gt
(greater than), and #ifver_gte
(greater than or equal to). The source code inside of the curly braces will only be included in the compiled file if the current OpenGL version matches that specified by the directive. As an example, the directive #ifver_gte "4.0" { ... }
will only include the braced code when running OpenGL 4.0 or greater.
The optional es
qualifier specifies that the code block should only be included if running OpenGL ES.
Indicates that the specified texture uniform should be bound to the sampler with the specified index.
When a ShaderSource
instance is processed using the static ShaderSource.ProcessExterns()
method, all #extern foo
declarations are expanded to #define foo value
. The value of each extern is specified by a dictionary which is passed to the method.
Provides a hint to the compiler that a uniform with the specified name must exist and must be associated with a corresponding EffectParameter
instance. If the uniform in question does not exist, an exception will be raised at load time. This directive can be useful in debugging scenarios, as uniforms which are removed by the shader optimizer will not be invisible to Ultraviolet.
Provides a hint to Ultraviolet that it should associate the specified shader uniform with a camera parameter. The default 3D rendering system can automatically populate these uniforms with values provided by the current camera object. Any string can be specified for <parameter>
, but only Ultraviolet's built-in camera parameters can be handled automatically. These are:
-
World
- The current world matrix. -
View
- The current view matrix. -
Projection
- The current projection matrix. -
ViewProj
- The combined world-view matrix. -
WorldViewProj
- The combined world-view-projection matrix.
For example, the #camera(ViewProj) "myviewproj"
specifies that the uniform called myviewproj
should be automatically populated with the current camera's combined view-projection matrix during rendering.
- Contributing
- Dependencies
- Basic Concepts
- First Look- Platform
- First Look- Graphics
- First Look- Audio
- First Look- Input
- First Look- Content
- First Look- UI
- sRGB Color
- Customizing SpriteBatch
- Creating Fonts
- Creating Effects
- Creating Glyph Shaders
- FreeType2 Fonts
- Rendering 3D Models