Moving from WebGL1 to WebGL2 is mostly a shader language upgrade. The rendering pipeline is still familiar, but GLSL ES 3.00 changes enough syntax that old shaders often fail immediately. This guide explains the exact differences, shows side by side examples, and gives you a safe migration checklist for existing shader code.
The short answer is that webgl2 glsl 300 es means two things: you request a webgl2 context in JavaScript, and your shaders use GLSL ES 3.00 syntax. In practice, that means adding #version 300 es, replacing attribute and varying, declaring your own fragment output, and switching old texture lookup calls such as texture2D() to texture().
| Topic | WebGL1 / GLSL ES 1.00 | WebGL2 / GLSL ES 3.00 |
|---|---|---|
| Context | canvas.getContext('webgl') | canvas.getContext('webgl2') |
| Version line | Usually omitted | #version 300 es must be the first line |
| Vertex inputs | attribute | in |
| Vertex to fragment data | varying | Vertex out, fragment in |
| Fragment colour output | gl_FragColor | User-declared out variable |
| Texture lookup | texture2D(), textureCube() | texture() |
| Useful shader helpers | More limited | textureSize(), texelFetch(), gl_VertexID |
| Common advanced features | Often extension-based | Many are core, including VAOs, instancing and MRT |
What is webgl2 glsl 300 es in practice?
It is the standard shader language for WebGL2. The browser API change is small, but the shader source changes are strict. If you request a WebGL2 context and compile GLSL ES 1.00 code unchanged, the most common failures are old keywords, old texture functions, or writing to gl_FragColor instead of a named output.
const canvas = document.querySelector('canvas');
const gl = canvas.getContext('webgl2');
if (!gl) {
throw new Error('WebGL2 is not available in this browser.');
}
#version 300 es
// This line must be first. No blank line above it.
What changes first when you migrate a shader?
The first migration step is syntax, not maths. Your transforms, lighting, UV interpolation, and sampling logic can stay broadly the same. The important part is renaming the GLSL interfaces so the vertex shader and fragment shader still agree about what data is being passed between them.
WebGL1 vertex shader
attribute vec4 a_position;
attribute vec2 a_uv;
varying vec2 v_uv;
void main() {
gl_Position = a_position;
v_uv = a_uv;
}
WebGL1 fragment shader
precision mediump float;
uniform sampler2D u_texture;
varying vec2 v_uv;
void main() {
gl_FragColor = texture2D(u_texture, v_uv);
}
WebGL2 vertex shader
#version 300 es
in vec4 a_position;
in vec2 a_uv;
out vec2 v_uv;
void main() {
gl_Position = a_position;
v_uv = a_uv;
}
WebGL2 fragment shader
#version 300 es
precision mediump float;
uniform sampler2D u_texture;
in vec2 v_uv;
out vec4 outColor;
void main() {
outColor = texture(u_texture, v_uv);
}
Which GLSL 3.00 keyword changes matter most?
There are four changes that matter in almost every shader migration. These are the ones that break working WebGL1 code the fastest, and they are also the easiest to fix once you know the mapping.
| Old GLSL ES 1.00 | New GLSL ES 3.00 | Why it changed |
|---|---|---|
attribute | in | Vertex shader inputs use a clearer, modern interface keyword |
varying | Vertex out, fragment in | Interpolated data is now declared by direction |
gl_FragColor | out vec4 outColor; | Fragment outputs are explicit and extensible |
texture2D(), textureCube() | texture() | Sampling is unified under one function name |
What stays the same between WebGL1 and WebGL2 shaders?
The short answer is that the shader model still feels like WebGL. Vertex shaders still write gl_Position. Fragment shaders still calculate a colour per fragment. Uniforms still provide per-draw values. Attributes still pull data from buffers. Interpolated values still flow from vertex stage to fragment stage. The migration is mostly about syntax and access to better built-ins.
- You still need a vertex shader and a fragment shader for each program.
- You still bind buffers and describe attribute layouts in JavaScript.
- You still use uniforms for matrices, colours, samplers and per-draw constants.
- You still need a precision qualifier in fragment shaders.
- You still sample textures in normalised UV space when using
texture().
What new shader features in GLSL 3.00 are actually useful?
WebGL2 is more than a rename exercise. Once you are on GLSL ES 3.00, you gain access to genuinely useful shader helpers that remove awkward workarounds from WebGL1. The most practical examples for day to day shader work are exact texel access, texture size queries, and vertex ID based generation.
Example: textureSize() for post-processing kernels
This is useful when you need one-pixel offsets for blur, sharpen, edge detection, or any screen-space filter. In WebGL1, many tutorials passed texture width and height as uniforms. In WebGL2, the shader can query the bound texture dimensions directly.
#version 300 es
precision highp float;
uniform sampler2D u_image;
in vec2 v_uv;
out vec4 outColor;
void main() {
vec2 onePixel = 1.0 / vec2(textureSize(u_image, 0));
vec4 left = texture(u_image, v_uv - vec2(onePixel.x, 0.0));
vec4 centre = texture(u_image, v_uv);
vec4 right = texture(u_image, v_uv + vec2(onePixel.x, 0.0));
outColor = (left + centre + right) / 3.0;
}
Example: texelFetch() for exact texel reads
Use this when you do not want filtered, normalised UV sampling. It is especially useful for data textures, lookup tables, cellular automata, GPGPU-style passes, or any algorithm where the texel index itself matters.
#version 300 es
precision highp float;
precision highp int;
uniform sampler2D u_data;
out vec4 outColor;
void main() {
ivec2 pixel = ivec2(gl_FragCoord.xy);
outColor = texelFetch(u_data, pixel, 0);
}
Example: gl_VertexID for generated geometry
This is handy for full-screen passes, procedural meshes, or bufferless experiments. Instead of uploading a position buffer for a simple full-screen triangle, the vertex shader can derive positions from the built-in vertex index.
#version 300 es
const vec2 POSITIONS[3] = vec2[3](
vec2(-1.0, -1.0),
vec2( 3.0, -1.0),
vec2(-1.0, 3.0)
);
void main() {
gl_Position = vec4(POSITIONS[gl_VertexID], 0.0, 1.0);
}
How do multiple render targets change fragment shaders?
In WebGL1, multiple render targets were an extension-led workflow. In WebGL2, they are a normal part of the API. The practical shader change is that explicit fragment outputs scale better because you are no longer tied to one implicit output variable.
#version 300 es
precision highp float;
in vec2 v_uv;
layout(location = 0) out vec4 outAlbedo;
layout(location = 1) out vec4 outMask;
void main() {
outAlbedo = vec4(v_uv, 1.0, 1.0);
outMask = vec4(1.0, 0.0, 0.0, 1.0);
}
In practice, you only need this pattern for deferred rendering, picking buffers, or multi-target post-processing. For a simple single-output fragment shader, a single out vec4 outColor; is enough.
What JavaScript changes usually accompany the shader upgrade?
The shader syntax changes are the visible part, but most teams also clean up JavaScript state handling during the move to WebGL2. The most common quality-of-life improvement is using vertex array objects so attribute state is grouped and rebound cleanly.
const gl = canvas.getContext('webgl2');
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);
// bind buffers and set attribute pointers here
gl.bindVertexArray(null);
You do not have to rewrite your whole renderer to benefit from WebGL2. A safe path is to migrate one shader pair, one mesh path, and one texture sampling path first. Once that compiles cleanly, move the rest of the pipeline in batches.
Common migration mistakes and failure modes
#version 300 esis not the first line of the shader source.- You requested
webglbut compiled GLSL ES 3.00 shader code. - You still use
attributeorvaryingin upgraded shaders. - You still write to
gl_FragColorinstead of a declared output variable. - You still call
texture2D()ortextureCube(). - You forgot the fragment shader precision qualifier.
- You mixed integer and float types without explicit casts.
- The vertex shader
outand fragment shaderinnames or types do not match.
Pros and cons of moving shader code to WebGL2
Pros
- Cleaner, more modern shader syntax.
- Better built-ins for image processing and data access.
- Less dependence on optional extensions.
- Easier paths to MRT, 3D textures, instancing and VAOs.
- Better foundation for more advanced renderers.
Cons
- Old shaders will not compile until the syntax is updated correctly.
- You may need a fallback if you still support WebGL1-only environments.
- Mixed WebGL1 and WebGL2 code paths can get messy during migration.
- Integer samplers, explicit outputs and new built-ins add new debugging surfaces.
Should you upgrade from WebGL1 to WebGL2 just for shaders?
Usually yes, if you actively maintain the renderer. The syntax upgrade alone is manageable, and the long-term gain is a cleaner shader model with fewer extension checks. If your project depends on post-processing, data textures, procedural generation, or multiple render targets, WebGL2 is the more practical base going forward.
If your current code is stable, simple, and targeted at the widest possible compatibility range, the answer is more nuanced. WebGL2 does not make a badly structured renderer magically faster. It gives you a better API surface and better shader tools, but you still need to design the pipeline well.
WebGL2 shader migration checklist
- Request a
webgl2context, notwebgl. - Add
#version 300 esas the first line of every shader. - Replace
attributewith vertexin. - Replace
varyingwith vertexoutand fragmentin. - Replace
gl_FragColorwith a declared fragment output. - Replace
texture2D()and related calls withtexture(). - Retest every shader pair for matching interface names, types and precision.
FAQ
No. GLSL ES 3.00 is for WebGL2. If you request a WebGL1 context, your shaders must use the older language rules and built-ins supported by that context.
No. A straight syntax migration does not automatically improve GPU performance. The benefit is better tooling and access to features that can reduce workarounds, CPU overhead, or multipass complexity.
gl_FragColor in WebGL2? A declared fragment output variable replaces it. For a single output shader, the usual pattern is out vec4 outColor; and then assigning to outColor inside main().
texture2D() in WebGL2? texture() replaces the older texture lookup functions. The sampler type determines how the sampling behaves, so the function name is unified in GLSL ES 3.00.
Usually only the syntax and stage interfaces. Most maths, lighting and UV logic can be kept. Rewrite logic only when you want to adopt WebGL2-only features such as texelFetch(), textureSize(), MRT or gl_VertexID.
Conclusion
WebGL2 vs WebGL1 for shaders is mostly a story about cleaner interfaces and more capable GLSL. The migration is not hard once you know the four core changes: #version 300 es, in/out, explicit fragment outputs, and texture(). After that, WebGL2 starts to pay off with practical helpers like textureSize(), texelFetch() and more robust multi-pass rendering workflows.
