Shaders are programs that the GPU can evaluate that allow the
programmer to completely customized the final output of a GPU draw
call. The (chickadee graphics shader)
module provides an API for
building custom shaders.
Shaders are written in the OpenGL Shading Language, or GLSL for short. Chickadee aspires to provide a domain specific language for writing shaders in Scheme, but we are not there yet.
Shader programs consist of two components: A vertex shader and a fragment shader. A vertex shader receives vertex data (position coordinates, texture coordinates, normals, etc.) and transforms them as desired, whereas a fragment shader controls the color of each pixel.
Sample vertex shader:
#version 130 in vec2 position; in vec2 tex; out vec2 fragTex; uniform mat4 mvp; void main(void) { fragTex = tex; gl_Position = mvp * vec4(position.xy, 0.0, 1.0); }
Sample fragment shader:
#version 130 in vec2 fragTex; uniform sampler2D colorTexture; void main (void) { gl_FragColor = texture2D(colorTexture, fragTex); }
This manual will not cover GLSL features and syntax as there is lots of information already available about this topic.
One way to think about rendering with shaders, and the metaphor Chickadee uses, is to think about it as a function call: The shader is a function, and it is applied to some “attributes” (positional arguments), and some “uniforms” (keyword arguments).
(define my-shader (load-shader "vert.glsl" "frag.glsl")) (define vertices (make-vertex-array …)) (gpu-apply my-shader vertices #:color red)
See Rendering Engine for more details about the gpu-apply
procedure.
Shaders are incredibly powerful tools, and there’s more information about them than we could ever fit into this manual, so we highly recommend searching the web for more information and examples. What we can say, though, is how to use our API:
Compile vertex-source, the GLSL code for the vertex shader, and fragment-source, the GLSL code for the fragment shader, into a GPU shader program.
Compile the GLSL source code within vertex-source-file and fragment-source-file into a GPU shader program.
Read GLSL source from vertex-port and fragment-port and compile them into a GPU shader program.
Return #t
if obj is a shader.
Represents the absence shader program.
Return the metadata for the uniform name in shader.
Return a hash table of uniforms for shader.
Return a hash table of attributes for shader.
Return #t
if obj is an attribute.
Return the variable name of attribute.
Return the binding location of attribute.
Return the data type of attribute.
Return #t
if obj is a uniform.
Return the variable name of uniform.
Return the data type of uniform.
Return the current value of uniform.
The shader examples in this manual thus far have only shown uniforms defined using primitive types. However, GLSL shaders support user-defined compound structs, such as this one:
struct DirectionalLight { vec3 direction; vec3 ambient; vec3 diffuse; vec3 specular; }; uniform DirectionalLight light;
While light
is declared as a single uniform in the shader code,
OpenGL translates this into four uniforms in this case: One
uniform each member of the DirectionalLight
struct. This poses
a problem for sending Scheme data to the GPU. How can compound Scheme
data translate into compound uniform data on the GPU? The answer is
with shader types. Shader types are a special kind of Guile struct
that provide a one-to-one mapping between a Scheme data structure and
a shader struct.
Some example code will explain this concept best. Here is the Scheme
equivalent of the DirectionalLight
struct:
(define-shader-type <directional-light> make-directional-light directional-light? (float-vec3 direction directional-light-direction) (float-vec3 ambient directional-light-ambient) (float-vec3 diffuse directional-light-diffuse) (float-vec3 specular directional-light-specular) (float shininess directional-light-shininess))
The macro define-shader-type
closely resembles the familiar
define-record-type
from SRFI-9, but with one notable
difference: Each struct field contains type information. The type
must be one of several primitive types (documented below) or another
shader type in the case of a nested structure.
It is important to note that the names of the shader type fields must match the names of the struct members in the GLSL code, otherwise Chickadee will be unable to perform the proper translation.
As of this writing, this interface is new and experimental. It remains to be seen if this model is robust enough for all use-cases.
Primitive data types:
Either #t
or #f
.
An integer.
An unsigned integer.
A floating point number.
A 2D vector (see Vectors.)
A 3D vector (see Vectors.)
A color.
A matrix (see Matrices.)
A texture (see Textures.)
A special type that means that the data is for the client-side (Scheme-side) only and should not be sent to the GPU. Any object may be stored in a local field.
Define a new shader data type called <name>.
Instances of this data type are created by calling the
constructor procedure. This procedure maps each field to a
keyword argument. A shader data type with the fields foo
,
bar
, and baz
would have a constructor that accepts the
keyword arguments #:foo
, #:bar
, and #:baz
.
A procedure named predicate will test if an object is a <name> shader data type.
Fields follow the format (field-type field-name [field-getter]
[field-setter])
. field-type and field-name are required
for each field, but field-getter and field-setter are
optional.
Return #t
if obj is a shader data type object.