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 a fragment shader
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 avec4 v;
then the propertyv.xyz
will give me avec3
with the x and y and z values ofv
.struct PointLight
: Defines a structure representing a single point light in the sceneuniform PointLight pointLights[NUM_POINT_LIGHTS]
: defines an array ofPointLight
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).
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,
ABlinnPhongShaderModel assumes that the loaded shader has uniforms named "diffuse", "ambient", "specular", and "specularExp", and it has helper static functions `ABlinnPhongShaderModel.AddAppState()` which adds sliders to the control panel for each of these uniforms, and `ABlinnPhongShaderModel.attachMaterialUniformsToAppState(mat:AShaderMaterial)` which connects the values of these uniforms in a given material instance to said control panel sliders.