Write a GLSL TOP

From TouchDesigner 088 Wiki

Overview

The official GLSL documentation can be found at this address.

TouchDesigner088's main supported version of GLSL is 3.30.

For help on writing a GLSL 1.20 TOP, refer to the [077 Wiki].

A shader written for the GLSL TOP is generally a image based operation. It does essentially no geometry based work. For users who are familiar with writing 3D GLSL shaders, a GLSL TOP is simply a shader applied to a single quad that is drawn to cover up the entire viewport (also known as a full-screen-aligned quad). To simplify the subject, this guide will avoid the extra complexities involved in 3D rendering, and present the topic of writing a GLSL shader in a 2D world only, dealing only with pixels.

The shader code in a GLSL TOP is run once for every pixel that is getting output. It's the job of the shader writer to:

  • Sample the pixel(s) of the inputs, if any.
  • Do whatever math is needed to create the pixel color.
  • Output the pixel color to a self-declared out variable.

Changes from GLSL 1.20

Many of the changes to the GLSL language can read in the GLSL spec, or here.

Concepts

Outputting Color

Usually you will only need a pixel shader to create a functioning GLSL TOP.

A simple shader to start with is one that just sets every pixel to red.

 layout(location = 0) out vec4 fragColor;
 void main()
 {
    fragColor = vec4(1.0, 0.0, 0.0, 1.0);
 }

Simply place this code into a DAT and set the GLSL TOPs Pixel Shader parameter to this DAT.

Notice that fragColor is defined by the shader writer as the location where the color is output. This is different from GLSL 1.20 where you used the built in variable gl_FragColor.

Sampling Inputs

The next thing you will likely want to do is sample the pixels of the input TOP(s). A shader that copies the input TOPs image data (assuming it's a 2D texture) is:

 layout(location = 0) out vec4 fragColor;
 void main()
 {
    fragColor = texture(sTD2DInputs[0], vUV.st);
 }

There is a few things to take note of in this shader. First, vUV is an input variable into the pixel shader that is set for you and will contain the texture coordinate of the pixel. This is to replace gl_TexCoord[], which is no longer available in GLSL 3.30. This variable is only given if you don't supply a vertex shader. If you supply your own vertex shader than it is up to you to pass the texture coordinate through to the pixel shader. The values will smoothly interpolate across the entire 2D image. so when your drawing the middle pixel the value of vUV.st will be (0.5, 0.5). Next, notice that differently from GLSL 1.20, you don't need to declare your sampler variables. They are declared for you as arrays. Samplers are split based on their dimensions (2D, 3D, 2DArray, Cube). The sampler that refers to the TOP containing the first 2D texture sTD2DInputs[0]. Similarly, the 2nd 2D input would be called sTD2DInputs[1] and so on for any number of 2D inputs (the GLSL Multi TOP has unlimited inputs, however your video card has a limited number of textures that can be used in a shader).

The line texture(sTD2DInputs[0], vUV.st), samples the texture sTD2DInputs[0], at texture coordinate vUV.st. Since vUV.st changes for every pixel, we'll be sampling a different pixel from the input each time.

To visualize the values for vUV.st, try putting this shader into the GLSL TOP.

  layout(location = 0) out vec4 fragColor;
  void main()
  {
     fragColor = vec4(vUV.s, vUV.t, 0.0, 1.0);
  }

Samplers

Samplers are GLSL's name for a texture. Samplers are given to your GLSL program as arrays, split based on the texture's dimensionality (2D, 3D, 2DArray, Cube etc.). You can find out how many of each type are connected to the TOP using these constants:

 TD_NUM_2D_INPUTS
 TD_NUM_3D_INPUTS
 TD_NUM_2D_ARRAY_INPUTS
 TD_NUM_CUBE_INPUTS

If you change the number/type of inputs connected to your GLSL TOP, then the shader will recompile with new values for the above defines and below arrays. Regardless of which input a TOP is connected to, it will be collapse into an array of samplers based on it's dimensionality. The arrays are defined as follows (you don't need to declare these in your shader):

 uniform sampler2D sTD2DInputs[TD_NUM_2D_INPUTS];
 uniform sampler3D sTD3DInputs[TD_NUM_3D_INPUTS];
 uniform sampler2DArray sTD2DArrayInputs[TD_NUM_2D_ARRAY_INPUTS];
 uniform samplerCube sTDCubeInputs[TD_NUM_CUBE_INPUTS];

So for example say you have 5 inputs connected to your GLSL TOP, in this order: a 2D TOP, a 3D TOP, a 2D TOP, a Cube TOP, a 2D Array TOP. Then

 TD_NUM_2D_INPUTS = 2
 TD_NUM_3D_INPUTS = 1
 TD_NUM_2D_ARRAY_INPUTS = 1
 TD_NUM_CUBE_INPUTS = 1

And you can reference your inputs like this:

 texture(sTD2DInputs[0], vUV.st); // first 2D input
 texture(sTD2DInputs[1], vUV.st); // second 2D input, NOT the second input connected to the TOP though
 texture(sTD3DInputs[0], vUV.stp); // first 3D input
 texture(sTDCubeInputs[0], vUV.stp); // first cube input
 texture(sTD2DArrayInputs[0], vUV.stp); // first 2D array input

Built In Samplers

For convenience the following samplers are provided for you to use as needed:

 uniform sampler2D sTDNoiseMap;  // A 256x256 Red-only channel texture that has random data.
 uniform sampler1D sTDSineLookup; // A Red-only texture that goes from 0 to 1 in a sine shape.

Uniforms

A uniform is a value that stays the same for every pixel that is drawn. They are set using the Vectors 1 and Vectors 2 pages in the GLSL TOP. To use a uniform inside your shader, declare a uniform of the same name and size as the parameters you have set on the Vectors pages of the GLSL TOP. For example, lets say we want to make a shader that will create an image that is one solid color, but you don't want it hard coded into the shader (as we did in the first example).

 layout(location = 0) out vec4 fragColor;
 uniform vec4 uColor;
 void main()
 {
    fragColor = uColor;
 }

You can now set the Value parameters on the GLSL TOP for the Uniform Name uColor however you see fit (ie. export to them, use expressions, or set them by hand). The GLSL TOP will automatically update and create a new image to match the changing values.

Built in Uniforms

The GLSL TOP has built-in uniforms that may come in useful depending on the shader you are writing. You do not need to declare this uniforms, they are declared for you.

There are many arrays of this structure that gives information about input/output textures. The structure is defined as:

 struct TDTexInfo
 {
   vec4 res;         // contains (1.0 / width, 1.0 / height, width, height)
   vec4 depth;       // contains (1.0 / depth, depth, depthOffset, undefined)
 };

For each of the input sampler arrays (2D, 3D, 2DArray etc.), there is a parallel array of the above structure containing the information about each sampler.

 uniform TDTexInfo uTD2DInfos[TD_NUM_2D_INPUTS];
 uniform TDTexInfo uTD3DInfos[TD_NUM_3D_INPUTS];
 uniform TDTexInfo uTD2DArrayInfos[TD_NUM_2D_ARRAY_INPUTS];
 uniform TDTexInfo uTDCubeInfos[TD_NUM_CUBE_INPUTS];

So for example to get the width of the first 2D input, you could type:

 float w = uTD2DInfos[0].res.z; 

When the input is a texture that has depth (3D or 2D Array), then the depth variable will contain the depth, and the depthOffset. The depthOffset is the offset from the texture coordinate at the front of the texture to the texture coordinate of the slice of the input that was most recently updated. So if you wanted a TOP that always output the newest slice of a 3D texture use this shader

 layout(location = 0) out vec4 fragColor;
 void main()
 {
     // The center of the first slice is not located at 0, but rather halfway between 0 (the start of the first slice)
     // and 1.0 / depth (the end of the first slice)
     float firstSlice = uTD3DInfos[0].depth.x * 0.5;
     
     // now add the offset
     firstSlice += uTD3DInfos[0].depth.z;

     // now sample the texture
     fragColor = texture(sTD3DInputs[0], vec3(vUV.st, firstSlice));
 }

For 3D textures the depthOffset is always between 0 and 1. For 2D Arrays the offset is between 0 and (depth - 1), and will always be an integer.

For the output info of the texture, use this uniform. The depthOffset value will always be 0 though.

 uniform TDTexInfo uTDOutputInfo;

When outputting a 3D or 2D Array texture, this uniform holds the slice index that you are currently rendering to.

 uniform int uTDCurrentDepth; // Refer to 3D Textures and 2D Texture Arrays

When using the "Num Passes" parameter on the Common page of the GLSL TOP, it is often useful to know which pass you are currently rendering in the shader. You can do this by looking at the uniform

 uniform int uTDPass;  // The current render pass in the GLSL TOP, starts at 0 and counts up.

Built-in Function

These are TouchDesigner specific functions which are made available for use within the shader.

Perlin and Simplex Noise

 // Noise functions
 // These will return the same result for the same input
 // Results are between -1 and 1
 // Can be slow so just be aware when using them. 
 // Different dimensionality selected by passing vec2, vec3 or vec4. 
 float TDPerlinNoise(vec2 v);
 float TDPerlinNoise(vec3 v);
 float TDPerlinNoise(vec4 v);
 float TDSimplexNoise(vec2 v);
 float TDSimplexNoise(vec3 v);
 float TDSimplexNoise(vec4 v);

HSV Conversion

 // Converts between RGB and HSV color space
 vec3 TDHSVToRGB(vec3 c);
 vec3 TDRGBToHSV(vec3 c);

Dithering

 // Applies a small random noise to the color to help avoid banding
 // in some cases.
 vec4 TDDither(vec4 color);

Sampling more than one pixel

It some shaders you may want to sample more than one pixel from the input TOP (when creating a Blur shader for example). This is done simply with multiple calls to texture(), while offsetting the values of vUV (or your own texture coordinate).

In texture coordinate terms, the value difference between one pixel and the pixel directly to the right of it is (1.0 / width). Similarly, the value difference between one pixel and the pixel directly below it is -(1.0 / height). The following function is helpful in calculating the correct texture coordinates for neighboring pixels:

  // This function is not provided for you, you need to declare it yourself.
  vec2 input2DOffset(int texIndex, int xOffset, int yOffset)
  {
      return vec2(vUV.s + (float(xOffset) * uTD2DInfos[texIndex].res.s),
                 vUV.t + (float(yOffset) * uTD2DInfos[texIndex].res.t));
  }


There is however a new function is GLSL, textureOffset() which does this work for you. It has a limited range it can sample from the starting coordinate though, so it can't be used to get an arbitrary sample offset from a coordinate.

Here is a very simple blur shader that will sample a 3x3 grid around each pixel and output the average value of all 9 pixels. It's manually calculating the offsets instead of using textureOffset, although textureOffset would work fine in this example since the offsets are only 1 pixel.

 vec2 input2DOffset(int texIndex, int xOffset, int yOffset)
 {
     return vec2(vUV.s + (float(xOffset) * uTD2DInfos[texIndex].res.s),
                vUV.t + (float(yOffset) * uTD2DInfos[texIndex].res.t));
 }
 
layout(location = 0) out vec4 fragColor; void main() { vec4 colorSum = vec4(0.0); colorSum += texture(sTD2DInputs[0], input2DOffset(0, 0, 0)); colorSum += texture(sTD2DInputs[0], input2DOffset(0, -1, -1)); colorSum += texture(sTD2DInputs[0], input2DOffset(0, 0, -1)); colorSum += texture(sTD2DInputs[0], input2DOffset(0, 1, -1)); colorSum += texture(sTD2DInputs[0], input2DOffset(0, 1, 0)); colorSum += texture(sTD2DInputs[0], input2DOffset(0, 1, 1)); colorSum += texture(sTD2DInputs[0], input2DOffset(0, 0, 1)); colorSum += texture(sTD2DInputs[0], input2DOffset(0, 0, -1)); colorSum += texture(sTD2DInputs[0], input2DOffset(0, -1, 0));
fragColor = colorSum / 9.0; }

3D Textures and 2D Texture Arrays

When creating a 3D Texture or a 2D Texture Array, your shader will be rendered once for every depth slice that is created. It's like rendering a bunch of 2D textures.

Along with the different input samplers you'll get, you also have access to a few uniforms to help you decide what to create for each slice.

 uniform int uTDCurrentDepth; // Is the 0-based index of the slice that's currently being created.

Outputting to Multiple Color Buffers

In the same pixel shader you can output to multiple identical size/format buffers at the same time. To do this first turn up the "# of Color Buffers" parameter in the GLSL TOP to the number of outputs you need. Then in your shader declare your other other output locations. For example if your plan to output to 3 different buffers you could declare them like this:

 layout(location = 0) out vec4 fragColor;
 layout(location = 1) out vec4 otherColor;
 layout(location = 2) out vec4 extraInfo;

Now you can write to fragColor, otherColor and extraInfo to write to the 3 color buffers that your are outputting to. If you don't write to all of your outputs in all cases, the resulting pixel value is undefined. Don't avoid writing a value to try to keep last frame's value in the buffer.

The output connector on the GLSL TOP will always output the color for location=0. To get the other color buffers use a Render Select TOP and point it to the GLSL TOP, then select your color buffer index you want.

Vertex Shader

In most cases you will not need to provide a vertex shader to the GLSL TOP. If you decide to provide a vertex shader, it's most basic form would be:

 out vec3 texCoord;
 void main()
 {
      texCoord = uv[0];
      gl_Position = TDSOPToProj(vec4(P, 1.0));
 }

It is very important that you do not manipulate the vertex position, as it will cause the quad to not be aligned with the TOP output. Also, notice how we declare our own output variable for the texture coordinate here. vUV will not be automatically available to us in the pixel shader if we supply a vertex shader, so we use this variable instead;

 layout (location = 0) out vec4 fragColor;
 in vec3 texCoord;
 void main()
 {
     fragColor = texture(sTD2DInputs[0], texCoord.st);
 }

Other Notes

#version statement

TouchDesigner will automatically put a #version statement at the start of the shaders when compiling them, so you should make sure your shaders don't have a #version statement. You will get an error if they do.