Writing Shader Templates
pySSV has a powerful shader templating system which aims to reduce boilerplate code and improve compatibility of shaders with different rendering backends.
When a user writes a shader they specify which shader template to compile with using the #pragma SSV <template_name>
directive. The preprocessor searches for the template specified by the user in the following order:
Templates passed in to the
shader()
method along with the source code.Templates in the folder passed in to the
shader()
method.Templates in the built in templates folder (packaged with pySSV).
Shader templates must have a filename in the form template_<template_name>.glsl
to be found. For instance, if the
following shader source code is passed in the shader()
method:
#pragma SSV pixel mainImage
vec4 mainImage(in vec2 fragCoord)
{
vec2 uv = fragCoord/uResolution.xy;
vec3 col = sin(uv.xyx + iTime * vec3(3, 4, 5)) * 0.5 + 0.5;
return vec4(vec3(col), 1.0);
}
The shader preprocessor will search for the template: template_shadertoy.glsl
this template is parsed to determine
to parse further arguments in the #pragma SSV <template_name> ...
directive. The template file is then preprocessed
with the user’s source code and template parameters injected in to the template.
In this example the pixel
shader template looks like this:
#pragma SSVTemplate define pixel --author "Thomas Mathieson" --description "A simple full screen pixel shader."
#pragma SSVTemplate stage vertex
#pragma SSVTemplate stage fragment
// Arguments get converted into compiler defines by the preprocessor
// an argument's name is transformed to match our naming convention:
// entrypoint -> T_ENTRYPOINT
// _varying_struct -> T_VARYING_STRUCT
#pragma SSVTemplate arg entrypoint -d "The name of the entrypoint function to the shader."
#pragma SSVTemplate arg _z_value -d "The constant value to write into the depth buffer. 0 is close to the camera, 1 is far away." --default 0.999
// Prefixing an argument name with an underscore is shorthand for --non_positional
// #pragma SSVTemplate arg _varying_struct --type str
// An example for an SDF shader
// #pragma SSVTemplate arg _render_mode --choices solid xray isolines 2d
// Include any default includes we think the user might want
// compat.glsl automatically declares the #version and precision directives when needed, it should always be the
// first file to be included in the template.
#include "compat.glsl"
// global_uniforms.glsl contains the declarations for all uniforms which are automatically passed in by pySSV
#include "global_uniforms.glsl"
// Use these preprocessor blocks to specify what code to compile for each shader stage.
// These macros (SHADER_STAGE_<stage_name>) are defined automatically by the preprocessor.
#ifdef SHADER_STAGE_VERTEX
layout(location = 0) in vec2 in_vert;
layout(location = 1) in vec3 in_color;
layout(location = 0) out vec3 color;
layout(location = 1) out vec2 position;
void main() {
gl_Position = vec4(in_vert, (T_Z_VALUE)*2.-1., 1.0);
color = in_color;
position = in_vert*0.5+0.5;
}
#endif //SHADER_STAGE_VERTEX
#ifdef SHADER_STAGE_FRAGMENT
out vec4 fragColor;
layout(location = 0) in vec3 color;
layout(location = 1) in vec2 position;
// Including the magic string "TEMPLATE_DATA" causes the user's shader code to be injected here.
#include "TEMPLATE_DATA"
void main() {
// T_ENTRYPOINT is a macro that was defined automatically when the argument defined
// by '#pragma SSVTemplate arg entrypoint' was passed in.
fragColor = T_ENTRYPOINT(position * uResolution.xy);
// Despite the explicit layout, sometimes in_color still gets stripped...
fragColor.a += color.r*1e-20;
}
#endif //SHADER_STAGE_FRAGMENT
When preprocessing the template the arguments passed in to the #pragma SSV <template_name>
directive are converted
to preprocessor defines. Argument names are converted to uppercase and prefixed with T_
, so the argument
entrypoint
is passed in to the shader template as #define T_ENTRYPOINT <value>
. The glsl source passed in by
the user to the template is injected in the shader template using the special #include "TEMPLATE_DATA"
directive
which simply expands to the user’s glsl code when preprocessed. Arguments are passed in to the shader as defines
exactly as they are specified by the user:
// If the user specifies these arguments
#pragma SSV sdf sdf_main --camera_speed -1.5 --light_dir "normalize(vec3(0.1, 0.2, 0.3))"
// They will be defined by the preprocessor as
#define T_ENTRYPOINT sdf_main
#define T_CAMERA_SPEED -1.5
// Notice in this case that to specify a value which contains whitespace, it must be wrapped in quotation marks.
// A few basic c++ style escape sequences are supported in this case as well (\", \n, \t).
#define T_LIGHT_DIR normalize(vec3(0.1, 0.2, 0.3))
SSVTemplate
Directives
The template is parametrised using #pragma SSVTemplate
directives.
Define Directive
#pragma SSVTemplate define
This directive is used to define a shader template and any metadata associated with it.
Parameters:
- name
The name of the shader template. This should only consist of characters valid in filenames and should not contain spaces.
- --author
The shader template’s author.
- --description
A brief description of the shader template and what it does.
Stage Directive
#pragma SSVTemplate stage
This directive specifies a shader stage to compile this template for.
Parameters:
- stage
The stage(s) to compile for. Accepts one or more of:
vertex
,fragment
,tess_control
,tess_evaluation
,geometry
, orcompute
.
Arg Directive
#pragma SSVTemplate arg
This directive defines an argument to be passed in to the shader template in the #pragma SSV <template_name> [args]
directive.
Parameters:
- name
The name of the argument to be passed in to the shader; prefixing the name with an underscore implies the
--non_positional
flag.
- --non_positional
[Flag] Treat this as a non-positional argument; it’s name is automatically prefixed with
--
.
- --action
What to do when this argument is encountered. Accepts the following options:
store
(default) Stores the value the user passes in to the argument in the argument.store_const
Stores a constant value (defined in--const
) in the argument when this flag is specified.store_true
A special case ofstore_const
which storestrue
when the flag is specified andfalse
if it isn’t.store_false
The inverse ofstore_true
.
- --default
The default value for this argument if it isn’t specified.
- --choices
Limits the valid values of this argument to those specified here. This parameter accepts multiple values. The choices are defined as compiler macros allowing you to test for choices as follows:
#pragma SSVTemplate arg _camera_mode --choices INTERACTIVE AUTO #if T_CAMERA_MODE == AUTO ...
- --const
When using the ‘store_const’ action, specifies what value to store.
Input Primitive Directive
#pragma SSVTemplate input_primitive
This directive allows the shader to specify what type of OpenGL input primitive it’s expecting. If this directive is not
specified, the renderer defaults to TRIANGLES
.
Parameters:
- primitive_type
The primitive type for the renderer to dispatch the shader with. Accepts one of the following options:
POINTS
treat the input vertices as points.LINES
treat the input vertices as an array of line segments; each line consumes 2 vertices.LINE_LOOP
LINE_STRIP
(unsupported) treat the input vertices as a line strip; each line consumes 1 vertex.TRIANGLES
(default) treat the input vertices as a an array of triangles; each triangle consumes 3 vertices.TRIANGLE_STRIP
TRIANGLE_FAN
LINES_ADJACENCY
LINE_STRIP_ADJACENCY
TRIANGLES_ADJACENCY
TRIANGLE_STRIP_ADJACENCY
PATCHES