Previous: , Up: Graphics   [Contents][Index]


2.3.15 Shaders

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 …))
(shader-apply my-shader vertices #:color red)

See Rendering Engine for more details about the shader-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:

Procedure: strings->shader vertex-source fragment-source

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.

Procedure: load-shader vertex-source-file fragment-source-file

Compile the GLSL source code within vertex-source-file and fragment-source-file into a GPU shader program.

Procedure: make-shader vertex-port fragment-port

Read GLSL source from vertex-port and fragment-port and compile them into a GPU shader program.

Procedure: shader? obj

Return #t if obj is a shader.

Variable: null-shader

Represents the absence shader program.

Procedure: shader-uniform shader name

Return the metadata for the uniform name in shader.

Procedure: shader-uniforms shader

Return a hash table of uniforms for shader.

Procedure: shader-attributes shader

Return a hash table of attributes for shader.

Procedure: shader-uniform-set! shader uniform value

2.3.15.1 Attributes

Procedure: attribute? obj

Return #t if obj is an attribute.

Procedure: attribute-name attribute

Return the variable name of attribute.

Procedure: attribute-location attribute

Return the binding location of attribute.

Procedure: attribute-type attribute

Return the data type of attribute.

2.3.15.2 Uniforms

Procedure: uniform? obj

Return #t if obj is a uniform.

Procedure: uniform-name uniform

Return the variable name of uniform.

Procedure: uniform-type uniform

Return the data type of uniform.

Procedure: uniform-value uniform

Return the current value of uniform.

2.3.15.3 User-Defined Shader Types

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:

Variable: bool

Either #t or #f.

Variable: int

An integer.

Variable: unsigned-int

An unsigned integer.

Variable: float

A floating point number.

Variable: float-vec2

A 2D vector (see Vectors.)

Variable: float-vec3

A 3D vector (see Vectors.)

Variable: float-vec4

A color.

Variable: mat4

A matrix (see Matrices.)

Variable: sampler-2d

A texture (see Textures.)

Variable: local-field

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.

Syntax: define-shader-type <name> constructor predicate (field-type field-name [field-getter] [field-setter]) …

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.

Procedure: shader-data-type? obj

Return #t if obj is a shader data type object.


Previous: , Up: Graphics   [Contents][Index]