If you are moving from WebGL1 shaders to WebGL2, the core job is usually simple: switch from GLSL 1.00 to GLSL 3.00 ES, replace attribute and varying, stop using gl_FragColor, and update old texture function calls. This guide is a practical migration cheat sheet, written from the perspective of fixing real shader compile failures rather than explaining the whole history of WebGL.
The short answer is that attribute becomes in in the vertex shader, varying becomes out in the vertex shader and in in the fragment shader, and fragment colour output must be declared explicitly. If you are updating a renderer rather than a single shader, request a webgl2 context and treat the shader upgrade as a small but strict syntax migration.
See our web development guides if you are also updating the surrounding render pipeline, build tooling, or deployment setup.
What changes when you migrate from GLSL 1.00 to GLSL 3.00 ES?
In practice, the biggest breaking changes are not advanced features. They are the small syntax rules that stop a shader compiling. The most important ones are adding #version 300 es as the first line, renaming shader inputs and outputs, replacing gl_FragColor, and switching old sampler calls such as texture2D() to texture().
| GLSL 1.00 | GLSL 3.00 ES | What to do |
|---|---|---|
attribute | in | Use in for vertex shader inputs |
varying in vertex shader | out | Vertex shader passes data out |
varying in fragment shader | in | Fragment shader receives interpolated data |
gl_FragColor | Custom out variable | Declare your own fragment output |
texture2D(), textureCube() | texture() | Use the unified texture sampling function |
| No version directive | #version 300 es | Must be the very first line |
If you are using AI to help port older shaders, do not treat the first working draft as the finished answer. Syntax swaps like attribute to in, varying to in or out, and replacing gl_FragColor are the obvious bits. The bigger risk is introducing subtle regressions around precision, pipeline expectations, naming, or compatibility. A sensible workflow is to pair the migration with lightweight code review automation so refactors get checked properly before they spread through a real project.
How do you convert attribute to in?
The short answer is that every vertex attribute in GLSL 1.00 becomes an in variable in GLSL 3.00 ES. This only applies to the vertex shader. Fragment shaders do not receive attributes directly, so any old attribute declaration belongs either in the vertex stage or in your JavaScript attribute binding code, not in the fragment shader.
// GLSL 1.00 vertex shader
attribute vec3 a_position;
attribute vec3 a_normal;
attribute vec2 a_uv;
// GLSL 3.00 ES vertex shader
#version 300 es
in vec3 a_position;
in vec3 a_normal;
in vec2 a_uv;
If you want stable attribute locations across programs, GLSL 3.00 ES also lets you assign them directly in the shader. That reduces surprises when reusing geometry and vertex array objects.
#version 300 es
layout(location = 0) in vec3 a_position;
layout(location = 1) in vec3 a_normal;
layout(location = 2) in vec2 a_uv;
How do you convert varying to in and out?
This is the part that trips people up most often. In GLSL 1.00, the same varying keyword appeared in both shader stages. In GLSL 3.00 ES, the variable direction is explicit. The vertex shader writes values with out, and the fragment shader reads those same values with in.
// GLSL 1.00 vertex shader
attribute vec3 a_position;
attribute vec2 a_uv;
varying vec2 v_uv;
void main() {
v_uv = a_uv;
gl_Position = vec4(a_position, 1.0);
}
// GLSL 1.00 fragment shader
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_diffuse;
void main() {
gl_FragColor = texture2D(u_diffuse, v_uv);
}
// GLSL 3.00 ES vertex shader
#version 300 es
in vec3 a_position;
in vec2 a_uv;
out vec2 v_uv;
void main() {
v_uv = a_uv;
gl_Position = vec4(a_position, 1.0);
}
// GLSL 3.00 ES fragment shader
#version 300 es
precision mediump float;
in vec2 v_uv;
uniform sampler2D u_diffuse;
out vec4 outColor;
void main() {
outColor = texture(u_diffuse, v_uv);
}
Two rules matter here. The names and types must still match across stages, and the fragment shader input only works if the vertex shader writes the corresponding output. If either side is missing, the shader may compile but the program can fail to link cleanly.
What replaces gl_FragColor in GLSL 300 es?
You no longer write to gl_FragColor in GLSL 3.00 ES. Instead, you declare your own fragment output variable with out. For a single render target, one output variable is enough. For multiple render targets, declare more than one output and assign locations where needed.
#version 300 es
precision mediump float;
out vec4 outColor;
void main() {
outColor = vec4(1.0, 0.0, 0.0, 1.0);
}
If your old shader used gl_FragData, the modern replacement is multiple named outputs. That is cleaner, easier to debug, and fits WebGL2 much better than the old implicit fragment output model.
What replaces texture2D() and textureCube()?
GLSL 3.00 ES replaces most old texture lookup function names with a single texture() function. The sampler type determines the correct behaviour. This means you stop calling texture2D() and textureCube() and use texture() consistently.
// Old
vec4 base = texture2D(u_diffuse, v_uv);
vec4 env = textureCube(u_environment, v_dir);
// New
vec4 base = texture(u_diffuse, v_uv);
vec4 env = texture(u_environment, v_dir);
This is a small change, but it appears in a lot of shaders. If you are migrating a large codebase, it is worth doing this replacement systematically rather than fixing files one by one after compile errors appear.
What is the minimum WebGL2 shader template that works?
If you only need a reliable starting point, use this pair as your baseline. It includes the version directive, explicit vertex inputs and outputs, fragment precision, and a declared colour output. In practice, getting to a known-good template first is much faster than patching an older broken shader line by line.
// Vertex shader
#version 300 es
in vec3 a_position;
in vec2 a_uv;
out vec2 v_uv;
void main() {
v_uv = a_uv;
gl_Position = vec4(a_position, 1.0);
}
// Fragment shader
#version 300 es
precision mediump float;
in vec2 v_uv;
uniform sampler2D u_texture;
out vec4 outColor;
void main() {
outColor = texture(u_texture, v_uv);
}
What JavaScript changes usually go with a GLSL 300 es migration?
The shader syntax update usually sits inside a broader move from WebGL1 to WebGL2. The first required JavaScript change is requesting a webgl2 context rather than webgl. After that, your attribute bindings, uniform uploads, and draw calls may stay mostly the same unless you also adopt newer WebGL2 features.
const gl = canvas.getContext('webgl2');
That said, migration work often becomes cleaner if you also adopt vertex array objects, stable attribute locations, and stricter program setup. Those are not required just to fix shader compilation, but they usually make the upgrade less fragile.
Common migration errors that break GLSL 300 es shaders
Most failed migrations come down to a handful of strict rules. These are the ones to check first when a shader suddenly stops compiling or linking after you switch versions.
#version 300 esis not the first line- You left
attributeorvaryingin the shader - You still write to
gl_FragColor - You still call
texture2D()ortextureCube() - The vertex shader uses
outbut the fragment shader input name or type does not match - You removed the fragment shader precision qualifier
- You requested a
webglcontext but compiled GLSL 300 es shaders - You mixed old WebGL1 assumptions about attribute locations with new shader code
Before and after: full minimal migration example
This is the fastest way to see the whole change in one place. The example keeps the logic identical and only updates the shader language and output rules. That is usually how a safe first migration should look: keep behaviour the same, modernise syntax, then refactor later.
// Before: GLSL 1.00
// vertex
attribute vec3 a_position;
attribute vec2 a_uv;
varying vec2 v_uv;
void main() {
v_uv = a_uv;
gl_Position = vec4(a_position, 1.0);
}
// fragment
precision mediump float;
varying vec2 v_uv;
uniform sampler2D u_texture;
void main() {
gl_FragColor = texture2D(u_texture, v_uv);
}
// After: GLSL 3.00 ES
// vertex
#version 300 es
in vec3 a_position;
in vec2 a_uv;
out vec2 v_uv;
void main() {
v_uv = a_uv;
gl_Position = vec4(a_position, 1.0);
}
// fragment
#version 300 es
precision mediump float;
in vec2 v_uv;
uniform sampler2D u_texture;
out vec4 outColor;
void main() {
outColor = texture(u_texture, v_uv);
}
Operational migration checklist
If you want the shortest route from failing WebGL1 shaders to a working WebGL2 build, use this checklist in order.
- Request a
webgl2context - Add
#version 300 esas the first line of every migrated shader - Replace vertex
attributedeclarations within - Replace
varyingwith vertexoutand fragmentin - Declare a fragment output such as
out vec4 outColor; - Replace
texture2D()and similar calls withtexture() - Recheck matching names, matching types, and fragment precision qualifiers
FAQ
Can you use attribute in GLSL 300 es?
No. In GLSL 3.00 ES, vertex shader inputs use in, not attribute.
What does varying become in GLSL 300 es?
It becomes out in the vertex shader and in in the fragment shader.
Why does my GLSL 300 es shader fail even after I changed attribute and varying?
The most common reasons are that #version 300 es is not the first line, gl_FragColor is still present, or old texture functions such as texture2D() are still in the fragment shader.
Do you still need a precision qualifier in the fragment shader?
Yes. In WebGL and WebGL2 fragment shaders, precision qualifiers such as precision mediump float; are still normally required.
Is GLSL 300 es the same as desktop GLSL 3.30?
No. They are related but not identical. WebGL2 uses GLSL ES 3.00, so use syntax and features valid for the ES version rather than assuming desktop GLSL behaviour.
Final take
The short answer is that most GLSL 1.00 to 3.00 ES migrations are syntax migrations, not rendering rewrites. Replace attribute with vertex in, split varying into vertex out and fragment in, declare a fragment output, and switch to texture(). Once those are in place, most simple WebGL1 shaders can be brought over to WebGL2 cleanly.
Reference
For the underlying language and API reference, see the Khronos WebGL documentation: Khronos WebGL.
