Skip to main content

Shaders

-"Let's throw some shade"

AniGraph greatly simplifies the process of writing and using custom shaders in ThreeJS. One potential feature for your final project is to take advantage of this to realize some cool visual effects.

How Shaders Work in AniGraph & ThreeJS

First, some things to know:

  • When you use rasterization-based graphics, there is always *some* kind of shader being used. Systems that don't ask you to write shaders are merely providing their own by default under the hood. Three.js is no exception.
  • When you use shaders in WebGL/Three.js/AniGraph, the shader itself is defined as a text file that is only compiled and moved to the GPU at runtime. This makes working with shaders a bit different from working with normal code.
  • For WebGL/Three.js/AniGraph, shaders are written in glsl, which is a limited c-like language with some helper functions that are useful in computer graphics.
  • As we discussed in class, there are actually two types of shaders: vertex shaders (___.vert.glsl) and fragment shaders (___.frag.glsl). Together, a vertex shader and a fragment shader define a shader program.

Where are the shaders?

In AniGraph, shader code are stored in the shaders directory of the public folder. Each subdirectory is named for its corresponding shader.

Shaders, Materials, Uniforms, and Varying attributes

In WebGL and AniGraph, a shader program is associated with a material, which is paired with geometry to define content for rendering. In AniGraph, shader materials are represented with the AShaderMaterial class, which extends AMaterial.

In many cases, we may want to use the same shader logic on many materials but with different parameters for different objects. For example, we may want to write a Phong shader that uses texture mapping for the diffuse component, but we may want to use a different texture map for different objects in our scene and we may want to adjust the strength of specular reflection for different objects. In shaders, these parameters that change for different materials using the same shader code are called "uniforms". I know; that seems like a confusing name since they are variables that we are changing, but they are called "uniform" because they are the same for all vertices and/or all fragments rendered with a given material. By contrast, "varying" variables are things that may change with each vertex or pixel---for example, normal vectors or vertex colors.

Let's look at some code to make this more concrete. Below is some code for declaring variables taken from the fragment shader defined in public/shaders/customexample1/customexample1.frag.glsl:




uniform float ambient;
uniform float diffuse;

//...

uniform sampler2D diffuseMap;
uniform bool diffuseMapProvided;

//...

varying vec4 vPosition;
varying vec3 vNormal;
varying vec2 vUv;
#ifdef USE_COLOR
varying vec4 vColor;
#endif

struct PointLight {
vec3 color;
vec3 position; // light position, in camera coordinates
float distance; // used for attenuation purposes.
float intensity;
float decay;
};
uniform PointLight pointLights[NUM_POINT_LIGHTS];

Types

  • float: a floating point scalar.
  • vec2/vec3/vec4: 2-d, 3-d and 4-d vectors. If I have a vec4 v; then the property v.xyz will give me a vec3 with the x and y and z values of v.
  • struct PointLight: Defines a structure representing a single point light in the scene
  • uniform PointLight pointLights[NUM_POINT_LIGHTS]: defines an array of PointLight structs. These are provided through AniGraph and three.js so that when you add a new point light to the scene (there are examples of this in the provided scene models).
caution

Floating point values and integers are different types in glsl, and using one in place of another will cause compiling errors. In particular, if you type an integer like 2 in your code, this will be interpreted as an integer. If you want to interpret it as a float, you need to add a decimal point, i.e., 2.0. So if I try to declare a variable like float myFloat = 2; this will actually cause a compiling error, because I am trying to assign an integer value to a float variable.

Uniform Attributes

You can see some variables are declared as uniform, while others are declared as varying attributes, and both types are declared with their associated type. The uniform variables are uniform for all vertices/fragments rendered with this shader. For example, we use uniform float ambient to control the ambient light in our shader---in the example code, we connect this uniform to a control panel slider.

The variable uniform sampler2D diffuseMap represents a texture map, which we can sample at different texture coordinates in the shader. AniGraph generates and sets the uniforms _texname_Map and _texname_MapProvided when you set a texture on a given material using code like mat.setTexture('diffuse', diffuseTexture).

Varying Attributes

Since this is a fragment shader, variables marked with varying are arguments that vary for each fragment based on interpolation performed by the rasterizer before the fragment shader runs. For vertex shaders, varying means that a value can be different for each vertex. Variables marked with varying in the vertex shader are interpolated during rasterization to produce the corresponding variables in the fragment shader, so it's important that the names and types of these match between a fragment shader and its corresponding vertex shader.

Using Vertex Colors

In the example code above, the pre-processing macros #ifdef USE_COLOR and #endif are used to change the shader based on some condition. In this case, that condition is whether color values are provided for each vertex, and whether the material is told to use those color values. You can set whether vertex colors should be used on a given shader material by setting its usesVertexColors property, but only set this to true if your vertex data actually has color data.

Shader Models

To facilitate creating materials that run the same shader code but use different uniforms, AniGraph offers the AShaderModel class, which you can think of as a factory for creating new materials that use the same shader code. Some subclasses of AShaderModel are customized to particular shader code, or shader code that makes use of certain common uniform names.For example, ABasicShaderModel assumes that the loaded shader has uniforms named "diffuse" and "ambient", and it connects the ambient uniform to a slider control in the application control panel (you may find this useful for debugging!).

Creating Shader Models

To create a shader model, use AShaderModel.CreateModel(shaderName) where shaderName is the name of the shader matching the folder and file name prefixes in the shaders directory. Scene models contain a materials property that manages material models, so we can assign different models to different keys. In Example1SceneModel.ts, for example, we see an ABasicShaderModel being created for the shader under the public/shaders/customexample1/ directory:

let basicshader1ShaderMaterialModel = await ABasicShaderModel.CreateModel("customexample1");
await this.materials.setMaterialModel(MyMaterialNames.basicshader1, basicshader1ShaderMaterialModel);

Here the argument to CreateModel is the name of the shader (the same used for its source directory in public/shaders and the prefix of the code files inside the directory)

Creating a Shader Material using a Shader Model

Later in the scene model code, if we set the material to a key with this.materials.setMaterialModel, we can access the shader model using it's key with: matModel = this.materials.getMaterialModel(MyMaterialNames.basicshader1). To create a material instance based on a model, we use: mat = matModel.CreateMaterial(...args).

Setting uniforms and textures

You can set uniforms with mat.setUniform(name, value) and you can set textures with mat.setTexture(atexture).

Starting Shader

The starter code contains a few shaders that, at least to start with, pretty much all do the same thing. As an example, let's look closer at the code in public/shaders/customexample1/customexample1.frag.glsl. The void main() function is the entry point of the program, with the main task of setting the vec4 output variable gl_FragColor. Right now, the main function takes a weighted combination of some diffuse and specular component, which are returned by starter code functions that you will want to at least partially complete. Let's look closer at the evalDiffuse function, which you can use to evaluate the diffuse component:

vec3 evalDiffuse(vec3 position, vec3 N){

// we will use this to accumulate diffuse reflection from possibly multiple point lights
vec3 diffuseLighting = vec3(0.0,0.0,0.0);

if(NUM_POINT_LIGHTS>0){

// here we iterate over the point lights
for (int plight=0;plight<int(NUM_POINT_LIGHTS);++plight){

// position of current point light
vec4 lightPosition = vec4(pointLights[plight].position, 1.0);

// color of current point light
vec3 lightColor = pointLights[plight].color;

// The distance parameter in ThreeJS point lights is actually their range.
float lightRange = pointLights[plight].distance;

// The decay parameter controls how quickly the light decays over the specified range.
float lightDecay = pointLights[plight].decay;

// The falloff is computed like so...
vec3 pToL = lightPosition.xyz-vPosition.xyz;

// setting L to a normalized vector from the point we are rendering to the light
vec3 L = normalize(pToL);

// starter code simply sets diffuse strength to 1, discounting the angle of the light (you should fix this!)
float diffuseStrength = 1.0;

// we calculate a psuedo-physical falloff based on distance of the light from the point we are rendering
float dist = length(pToL);
float falloff = max(0.0, 1.0-(dist/lightRange));

falloff = pow(falloff, lightDecay);

// add the contribution of the current light to our accumulated diffuse light
diffuseLighting = diffuseLighting+diffuseStrength*falloff*lightColor;
}
}
return diffuseLighting;
}

You will get some small amount of credit for just completing these shaders to compute the angular falloff associated with diffuse reflection. A bit more for also completing the specular reflection code to get phong shading. To get a full point of credit you should go beyond this by implementing toon shading or other additional effects.