Repository navigation
Home
- 1. Shader Basics
- 2. Shader Preset (for THREE.ShaderMaterial)
- 3. Snippets
- 5. Diffrent methods for complex shaders
All technical research is recorded on our Miro!
Read The Book of Shaders.
A shader is a small program that runs on the GPU and controls how graphics are rendered. In GLSL, the two main types are vertex shaders and fragment shaders. The vertex shader processes the position and attributes of each vertex in 3D space, while the fragment shader calculates the final color of each pixel on the screen. It's important to note: the vertex shader can modify the positions of existing vertices but cannot create new vertices.
Shaders are used because they are highly efficient for parallel calculations. They run directly on the GPU, which is optimized for processing many vertices and pixels simultaneously. This makes shaders much faster than performing the same operations sequentially on the CPU.
In GLSL, shader code is written as if it processes one vertex or one fragment at a time. When a draw call is executed, the GPU runs many instances of the same shader in parallel, one for each vertex or fragment. These instances are independent and cannot communicate or share memory with each other.
© https://thebookofshaders.com
Because of this strict parallelism, data flow is tightly controlled. The vertex shader can output values that are passed to the fragment shader through varying variables (called as out in the vertex shader and in in the fragment shader). These values are different for each vertex and are automatically interpolated across fragments. They are called varyings because their values vary per vertex and per fragment, unlike uniforms, which remain constant for all shader executions.
Vertex and fragment shaders are connected in a rendering pipeline. The vertex shader runs first and processes each vertex individually. It can pass data (called varyings) to the fragment shader, which then runs for every pixel created from those vertices. This allows information like positions, normals, or texture coordinates to flow from vertices to fragments.
Uniforms are custom global variables shared by both vertex and fragment shaders. They are set from the CPU (for example via JavaScript) and remain constant during a draw call. Uniforms are used to inject external data such as time, resolution, colors, or transformation matrices into the shaders.
Attributes are per-vertex input data provided to the vertex shader. Common attributes include position, normal, and uv coordinates. Each vertex receives its own set of attribute values, meaning attributes can differ from vertex to vertex. Attributes only exist in the vertex shader, not in the fragment shader.
CPU / JavaScript
▼
Uniforms & Attributes
▼
Vertex Shader
▼
Fragment Shader
▼
Pixels
In computer graphics, a vertex passes through several coordinate spaces before it appears on the screen. It starts in model space, where the geometry is defined relative to its own origin. In world space, the object is positioned within the scene. View space represents the scene from the camera’s point of view, and finally clip space is used to determine what is visible on screen. In Three.js, the vertex shader usually multiplies the vertex position by the built-in matrices:
gl_Position = projectionMatrix * modelViewMatrix * vec4(v_position, 1.0);- Varyings are interpolated automatically between vertices
- Interpolation is linear in screen space
- This explains smooth color gradients, lighting, and textures
- GLSL supports
lowp,mediump, andhighp - Important for performance, especially on mobile GPUs
- Fragment shaders often need explicit precision declarations
- Common types:
float,vec2,vec3,vec4,mat4 - Vector operations are built-in and optimized
- Shaders are written in a math-oriented style
- The vertex count is a bottleneck because it needs to be processed in series on the CPU first
-
ifstatements exist but can be expensive - GPUs prefer branchless code
- Math-based conditions are often faster than logic
- Shaders themselves have no concept of time
- Time must be passed in via a uniform
- Used for animations, noise, and procedural effects
- Shaders are stateless
- No memory between frames
- Every frame is computed from scratch using inputs
Vertex Shader
precision mediump float;
// declare the varyings to pass data to the fragment shader
varying vec3 v_position;
varying vec3 v_normal;
void main() {
// Save the varyings
v_position = position;
v_normal = normalize(normalMatrix * normal);
// Vertex shader output
gl_Position = projectionMatrix * modelViewMatrix * vec4(v_position, 1.0);
}Fragment Shader
precision mediump float;
// Common uniforms
// uniform float u_time;
// uniform vec2 u_scroll;
// uniform vec2 u_resolution;
// varyings
varying vec3 v_position;
varying vec3 v_normal;
// include chunks
// #include <noisebase>
// #include <simplex3D>
// #include <perlin3D>
// #include <worley3D>
// #include <fbm>
// #include <curl>
void main() {
gl_FragColor = vec4(v_normal * 0.5 + 0.5, 1.0);
}Create Shader Material
new THREE.ShaderMaterial({
name: 'Preset',
uniforms: {
...sharedUniforms, // include uniforms
// Custom uniforms
},
vertexShader: vertexShader, // link Vertex Shader
fragmentShader: fragmentShader // link Fragment Shader
});For most of the 3D Noises, the position can me added to the time with a scalar. (4D Noises are ment to take the time in the 4th dimension):
vec3 p = v_position * 2.0 + 0.3 * u_time;Some noises need to be normalized from -1,1 to 0,1. This is also shown in: "Map from -1,1 to 0,1"
0.5 + 0.5 * n
// example
vec3 n = noise(v_position); // n: -1,1
gl_FragColor = vec4(vec3(0.5 + 0.5 * n), 1.0); // 0,1Most common: use F1 as grayscale
float n = cellular(v_position * 3.0).x;
gl_FragColor = vec4(vec3(n), 1.0);Use cell borders (F2 − F1)
vec2 c = cellular(v_position * 3.0);
float edge = c.y - c.x;
gl_FragColor = vec4(vec3(edge), 1.0);float n = cnoise(v_position * 2.0);
gl_FragColor = vec4(vec3(0.5 + 0.5 * n), 1.0);float n = cnoise(vec4(v_position * 2.0, 0.3 * u_time));
gl_FragColor = vec4(vec3(0.5 + 0.5 * n), 1.0);float n = snoise(v_position * 2.0);
gl_FragColor = vec4(vec3(0.5 + 0.5 * n), 1.0);Same as "Simplex 3D", but it returns additional information in "grad". Grad is a vector 3 that contains spatial derivative. That means the rate of change of a function with respect to spatial coordinates, so direction & strength.
vec3 grad;
float n = snoise(v_position * 3.0, grad);
gl_FragColor = vec4(vec3(0.5 + 0.5 * n), 1.0);float n = snoise(vec4(v_position * 2.0, 0.3 * u_time));
gl_FragColor = vec4(vec3(0.5 + 0.5 * n), 1.0);Fractal Brownian motion (fBm) takes a noise and adds octaves to it. That means multiple layers of the noise multiplied. With every octave, the base noise function is called again. With the "step", an offset can be added to each layer (diffrent in each layer).
vec3 pos = vec3(v_position.xy, 0.3 * u_time);
float n = fbm(pos, 5, 2.0, 0.4, vec2(0.0, 0.0));
gl_FragColor = vec4(vec3(0.5 + 0.5 * n), 1.0);This works on flat surfaces but not on spheres. For spheres, its best to map to spherical coordinates:
float scale = 1.0;
vec3 p = normalize(v_position);
float theta = atan(p.y, p.x);
float phi = acos(p.z);
vec3 sp = vec3(cos(theta) * sin(phi),
sin(theta) * sin(phi),
cos(phi)) * scale;
float n = fbm(sp, 5, 2.0, 0.4, vec2(0.0, 0.0));
gl_FragColor = vec4(vec3(0.5 + 0.5 * n), 1.0);(After the scale, motion can be added. For example: scale + 0.3 * u_time)
vec2 p = v_uv * 2.0;
vec2 q = p - vec2(0.5, 0.5);
// modify vec2 to move element
vec2 q = p - vec2(0.3, 0.6);0.5 + 0.5 * n
// example
vec3 n = noise(v_position); // n: -1,1
gl_FragColor = vec4(vec3(0.5 + 0.5 * n), 1.0); // 0,1float u = f * f * ( 3.0 - 2.0 * f );
float u = smoothstep( 0.0, 1.0, f );// raymarch definitions
#define STEPS 64 // loop steps
#define MAX_DIST 5.0 // far plane from camera -> cam.pos: 4.0 + radius: 0.5
#define SURF_DIST 0.001 // acurracy threshold
// signed distance to sphere -> SDF
float sdfSphere(vec3 p, float r) {
return length(p) - r; // returns where the current ray point is -> negative: in the sphere / positive: outside / 0: surface
}
// scene SDF
float map(vec3 p) {
return sdfSphere(p, 0.5);
}
// raymarch
float raymarch(vec3 ro, vec3 rd) {
float d = 0.0; // current traveled distance along ray
for (int i = 0; i < STEPS; i++) {
vec3 p = ro + rd * d; // get point at travel distance
float h = map(p); // check distance SDF
if (h < SURF_DIST || d > MAX_DIST) break; // break on hit or if nothing is hit after MAX_DIST
d += h; // jump to surface
}
return d; // return distance to hit
}
void main() {
// ray direction in object space
vec3 ro = v_camPos; // ray origin
vec3 rd = normalize(v_position - v_camPos); // ray direction
float d = raymarch(ro, rd); // find hit of every ray (distance from camera)
vec3 p = ro + rd * d; // hit position in space
float dist = map(p); // Signed distance to surface (in ASD position and scale)
// SURFACE
float surface = smoothstep(0.03, 0.0, abs(dist));
vec3 baseColor = vec3(0.29, 0.2, 0.53);
vec3 color = baseColor * surface * 1.5;
float alpha = clamp(surface, 0.0, 1.0);
gl_FragColor = vec4(color, alpha);
}| Name | Method | Performance | Look | Pros / Cons | Notes |
|---|---|---|---|---|---|
| Raymarched Volumetric | Use a bounding mesh (sphere, cube, capsule) and compute the object procedurally per fragment using raymarching. Procedural noise, turbulence, and color are calculated in the fragment shader. | Medium–Heavy | ★★★★★ | Pros: Fully 3D volumetric, dynamic, controllable. Cons: Shader complexity, GPU heavy. | Mesh is only a container; standard for procedural volumes; good for fully dynamic effects. |
| Textured / Layered Mesh | One or more meshes with ShaderMaterial using vertex displacement and procedural color/noise. Multiple overlapping layers can simulate depth and parallax. | High | ★★★★ | Pros: Lightweight, easy camera rotation, fakes volumetric depth. Cons: Limited internal volume; more layers = higher GPU cost. | Layering multiple meshes adds perceived volume; use additive blending and slight scale offsets for depth. |
| Particle System | Represent the object as many small moving particles or points with color, size, and opacity. Procedural noise can drive motion or flicker. Additive blending is often used. | High | ★★★★ | Pros: Fast, scalable, easy sparks/trails. Cons: Hard to achieve smooth volumetric look, may appear “blobby”. | Works well combined with a base mesh to give depth. |
