Index

Shaders and You!

Trial is based upon OpenGL and as such uses the GLSL language for writing shaders. While you can directly create shader instances and then compile them to a shader-program, the more typically intended user interface is through shader-entity classes, which are defined via define-shader-entity.

Shader Entities

A shader-entity holds shader code fragments in its class, and can inherit shader code fragments from its superclasses. Trial will combine the shaders together automatically, such that shader semantics combine like other method semantics would in CLOS. More on that later. Defining a basic custom shader class is simple:

(define-shader-entity my-object (vertex-entity)
  ())

(define-class-shader (my-object :fragment-shader)
  "out vec4 color;

void main(){
  color = vec4(0, 1, 0, 1);
}")

The above defines a class that has a fragment shader attached, which sets the output colour to green. We inherit from shader-entity which handles the loading and drawing of the mesh, and also provides the required vertex shader to process the vertices of the mesh and position them in space.

You can see the fully compiled shader with compute-effective-shaders:

(compute-effective-shaders (find-class 'my-object))

; ==>
(:VERTEX-SHADER "layout(location = 0) in vec3 position;
uniform mat4 model_matrix;
uniform mat4 view_matrix;
uniform mat4 projection_matrix;

void _GLSLTK_main_1(){
  gl_Position = (projection_matrix * (view_matrix * (model_matrix * vec4(position, 1.0))));
}

void main(){
  _GLSLTK_main_1();
}"
 :FRAGMENT-SHADER "out vec4 color;

void _GLSLTK_main_primary_1(){
  color = vec4(1.0, 1.0, 1.0, 1.0);
}

void _GLSLTK_main_1(){
  color = vec4(0, 1, 0, 1);
}

void main(){
  _GLSLTK_main_1();
}")

Here you can see the vertex shader we inherited, and the fragment shader we defined as well, though slightly modified by the merging. It appears alongside the vertex shader, we already inherited another default fragment shader which sets the output colour to white. In this case we just override the result so it doesn't matter much, but if we need to prevent a superclass' shader fragment, we can inhibit it:

(define-shader-entity my-object (vertex-entity)
  ()
  (:inhibit-shaders (shader-entity :fragment-shader)))

Looking at the effective shader for the class now, the inherited fragment is gone.

OpenGL offers many other shader types than fragment and vertex shaders, all of which you can attach to a shader entity in the same way. All of them can be specified with just raw GLSL code snippets. However, if so desired, you can also outsource the shader code into separate files, and then include them, instead:

(define-class-shader (my-object :fragment-shader)
  (pool-path 'my-project #p"my-object.frag"))

Or you can include a shader file with multiple sections directly in the class definition:

(define-shader-entity my-object (vertex-entity)
  ()
  (:shader-file (my-project "my-object.glsl")))

With the file being sectioned like this:

#section FRAGMENT_SHADER
out vec4 color;

void main(){
  color = vec4(0, 1, 0, 1);
}

You can also include other files, or the gl-source of assets from your shader with a #include:

#include (my-project:my-project "my-other-shader.glsl")
#include (trial:trial trial:standard-environment-information)

The acceptable include syntax being (POOL PATH), (POOL ASSET), CLASS-NAME, or PATHNAME where the path is relative to the current file.

There are currently no plans to include a more "lispy" syntax for writing shaders, though it isn't out of the question for such an inclusion to be made. Regardless, the raw GLSL approach will always be supported.

Trial however does make use of GLSL-Toolkit's "method combination" feature. If a function with the same signature is defined multiple times, the functions are renamed. That prevents inheriting multiple code parts from clashing. However, you can also call an earlier definition with call_next_method(); or maybe_call_next_method();.

Better still, you can add a @after, @before, or @around to the end of your function name to denote the same qualifier as you'd understand for CLOS' standard method combination. Such functions will then combine as expected with the rest in the same shader. Using this you can create much more modular interfaces for shaders and extend or override parts in a far more controlled manner.

GLSL-Toolkit will also ensure that function signature declarations are inserted before any function definitions, allowing you to refer to any other function without having to worry about function definition ordering.

Due to the looseness of GLSL code, the automated combination of fragments can sometimes end up somewhat unpredictable. Especially if the shader fragments do not agree on the names of various variables. GLSL-toolkit will do its best to try and match up the variables and rename them to slot the pieces together, but it may not always be successful at doing everything fully automated.

Currently we don't have a specified shader interface system to help with this issue, but it is something that we would like to add at a later point. For now, simply look at whatever fragments you inherit, and check that your variable names align, should the merging miscompile.

Please also note that under the typical render pipeline the effective shader not only depends on the fragments of your class, but can also be further modified by the shader passes it is rendered by.

Uniforms

Often you will want to pass extra information into a shader or configure parameters of it. This is usually done with uniform variables on the shader side, which are then set before the draw call is made. If you have such uniforms, you will often set them in a update-uniforms method like so:

(defmethod update-uniforms :after ((object my-object) program)
  (setf (uniform program "whatever") 10.0))

Note: setting uniforms that don't exist will quietly ignore, but trying to set a value that does not match the type of the uniform will error.

Trial support setting uniforms of type int, float, double, vec2, vec3, vec4, mat2, mat3, mat4, and matn. For more complex structure types you have to either use uniform buffer objects, or set each field of the structure individually by using its full name.

Sometimes uniforms don't change every frame and don't change for every object, and as such don't need to be updated with every render, either. In that case you have to gain access to the shader programs related to the entity via shader-program-for-pass and set the uniforms whenever they do change. However, this is usually an optimisation best left for later.

You can also more conveniently declare uniforms as slots, by adding the :uniform initarg to your slot definitions. If you specify T, the uniform name will be inferred via the symbol->c-name of the slot's name. Otherwise you can also directly specify the uniform name as a string. Note that this only takes care of setting the uniform value, not of declaring the uniform on the shader side.

Constants

Similar to uniforms you can define shader constants as shader entity slots, by adding the :constant initarg to your slot definitions. If you specify T, the constant name will be inferred via the upcased symbol->c-name of the slot's name. Otherwise you can also directly specify the constant name as a string.

The constant is added to the relevant shaders via compute-preprocessor-directives, which is called by make-class-shader-program for a shader-entity instance, and by make-pass-shader-program. The definitions are added at the top, right after the #version directive.

Please note that by default a per-object-pass groups shader programs together by the effective-shader-class of the object, and uses that class to compute the shader program. Thus, slot constants defined on the object will not be visible in the resulting shader program.