The point of a starter shader
ShaderMaterial is powerful because you control the vertex and fragment programs directly. That power also makes it easy to lose an hour to a black screen. A starter shader should prove that the geometry renders, uniforms update, UVs exist, and animation time is flowing. Only after those facts are visible should you add texture sampling, procedural patterns, or lighting math.
The best starter is not visually impressive. It is intentionally obvious. Use UV coordinates to create a gradient, mix in a color uniform, and animate a narrow band with uTime. If the band moves, JavaScript is updating the uniform. If the gradient appears, varyings are flowing from vertex to fragment. That gives you a stable base for more ambitious work.
Treat uniforms as a contract
A uniform is the small API between JavaScript and GLSL. Name it clearly and update it in one place. `uTime`, `uColor`, `uScale`, and `uResolution` are easier to reason about than a dozen anonymous values. When a shader becomes part of a tool, this contract lets sliders and presets change the shader without rewriting GLSL.
Avoid starting with too many uniforms. Three or four are enough for a reusable starter. Add values when the visual problem requires them. A shader page that teaches the contract is often more valuable than one that dumps a giant code block with no explanation.
Know what Three.js gives you
ShaderMaterial still runs inside the Three.js rendering pipeline. Built-in attributes such as position, normal, and uv are available when the geometry provides them. Projection and model-view matrices are also available. That means a beginner can start from a normal mesh and material swap rather than writing raw WebGL setup code.
The edge case is geometry without UV coordinates. A box, plane, or sphere from Three.js has usable UVs. Some imported models may not. If your shader depends on UVs, show a fallback color or document that the material expects UVs. This kind of plain warning saves future debugging.
Debug in layers
When the output is black, remove complexity. First set the fragment color to solid red. Then display `vUv.x` and `vUv.y` as colors. Then add the uniform color. Then add time. This layered method is boring, but it quickly tells you whether the problem is geometry, varyings, uniforms, or the final formula.
For a public guide, include the debugging sequence, not just the final shader. People do not search for ShaderMaterial because everything is already working; they search because the material is invisible, static, or different from an example.
Start from a visible wave shader
The generator publishes its complete default GLSL in the HTML and enhances it with a WebGL preview when available.
- Static code is readable without JavaScript
- uTime updates the wave
- Solid fallback guidance remains if WebGL fails
Prove fragment output before adding effects
A black shader is easier to isolate when the first test has no uniforms or texture lookups.
void main() {
float noise = texture2D(uNoise, vUv).r;
gl_FragColor = vec4(palette(noise + uTime), 1.0);
}void main() {
gl_FragColor = vec4(1.0, 0.2, 0.1, 1.0);
}
// Then display vUv, then add one uniform at a time.Default shader contract
The starter keeps the initial surface intentionally small and inspectable.
| Setting | Default |
|---|---|
| Pattern | Wave bands |
| Colors | #2f8f83 and #151b24 |
| Speed | 1.2 |
| Required uniforms | uTime, uColorA, uColorB |
Layer the shader in four passes
Each pass should compile and render before the next concern is introduced.
- Render a constant fragment color.
- Render UV coordinates as red and green.
- Add the color uniforms and verify their values.
- Add time animation last and check reduced-motion behavior.
Visible animated shader
const material = new THREE.ShaderMaterial({
uniforms: {
uTime: { value: 0 },
uColor: { value: new THREE.Color('#65d8c2') }
},
vertexShader: `
varying vec2 vUv;
void main() {
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform float uTime;
uniform vec3 uColor;
varying vec2 vUv;
void main() {
float band = 0.5 + 0.5 * sin((vUv.x + uTime * 0.2) * 12.0);
vec3 color = mix(vec3(vUv, 0.35), uColor, band);
gl_FragColor = vec4(color, 1.0);
}
`
});
Sources and further reading
Update record
- : Rewritten around a reproducible demo, explicit test record, code comparison, and known platform limits.