GLSL is not one language. It is a family of closely related languages: one for desktop OpenGL, one for phones and the web (GLSL ES), and a set of rules for Vulkan. They share almost every function and type, but a shader written for one is often rejected by another. This chapter shows how to tell them apart, how to move a shader between them, and how to keep one source that compiles everywhere.
By the end you can
- Read a #version line and say which API and which GPUs it is for.
- Port an old shader that uses attribute, varying and gl_FragColor.
- Write a fragment shader that compiles in WebGL 2 and on phones.
- Turn an OpenGL shader into a Vulkan one and compile it to SPIR-V.
- Keep one shader source for several targets with the preprocessor.
The first thing a GLSL compiler reads is the #version line. It says which edition of the language the rest of the file is written in, and the compiler then accepts exactly the words and rules of that edition. The number is the GLSL version times 100: #version 330 means GLSL 3.30.
Desktop GLSL versions follow OpenGL. Up to OpenGL 3.2 the numbers were different (OpenGL 2.0 came with GLSL 1.10), and from OpenGL 3.3 on they match: GLSL 3.30 is OpenGL 3.3, GLSL 4.60 is OpenGL 4.6. A desktop shader with no #version line at all is compiled as GLSL 1.10, the oldest edition, which is almost never what you want.
| #version | API | What it added |
|---|---|---|
| 110 | OpenGL 2.0 | the first GLSL; also what you get with no #version line |
| 120 | OpenGL 2.1 | non-square matrices such as mat2x3 |
| 130 | OpenGL 3.0 | in and out instead of attribute and varying, one texture() for every sampler, full integer support |
| 140 | OpenGL 3.1 | uniform blocks |
| 150 | OpenGL 3.2 | geometry shaders, interface blocks, the core and compatibility profiles |
| 330 | OpenGL 3.3 | layout(location = n) on vertex inputs and fragment outputs; the numbers now match OpenGL |
| 400 | OpenGL 4.0 | tessellation shaders, double |
| 420 | OpenGL 4.2 | layout(binding = n), image load and store, atomic counters |
| 430 | OpenGL 4.3 | compute shaders, shader storage buffers, layout(location) on uniforms |
| 450 | OpenGL 4.5 | dFdxFine and dFdxCoarse, cull distances |
| 460 | OpenGL 4.6 | gl_BaseVertex, gl_BaseInstance and gl_DrawID built in; OpenGL can load SPIR-V |
GLSL ES is the edition for OpenGL ES, the API of phones and tablets, and for WebGL, which is OpenGL ES inside a browser. Its version line ends in es, except the first one:
| #version | API | What it added |
|---|---|---|
| 100 (or no line) | OpenGL ES 2.0, WebGL 1 | the old style: attribute, varying, gl_FragColor, texture2D |
| 300 es | OpenGL ES 3.0, WebGL 2 | the modern style of desktop 3.30: in, out, texture(), layout(location) |
| 310 es | OpenGL ES 3.1 | compute shaders, storage buffers, image load and store; not in WebGL |
| 320 es | OpenGL ES 3.2 | geometry and tessellation shaders; not in WebGL |
From 1.50 on, a desktop #version line can name a profile: core or compatibility. Core contains only the modern language. Compatibility also keeps everything that was deprecated, such as gl_FragColor and gl_ModelViewMatrix, and works only with a compatibility OpenGL context. With no profile word, 1.50 and later mean core. This track and the OpenGL track always write core.
Much of the GLSL on the internet, and every GLSL ES 1.00 shader written for WebGL 1, still uses the words from before GLSL 1.30. They are not wrong in their own dialect, but a #version 330 core or a #version 300 es shader rejects them. Porting is mechanical:
| Old (1.10, 1.20, ES 1.00) | Modern (3.30, ES 3.00, Vulkan) | Note |
|---|---|---|
| attribute vec3 aPos; | layout(location = 0) in vec3 aPos; | vertex shader inputs |
| varying vec2 vUV; | out vec2 vUV; (vertex) · in vec2 vUV; (fragment) | one word became two, one per side |
| gl_FragColor = c; | out vec4 FragColor; … FragColor = c; | you declare the output and choose its name |
| gl_FragData[1] = n; | layout(location = 1) out vec4 Normal; | several render targets, as in deferred shading |
| texture2D, textureCube, shadow2D | texture | the sampler's type picks the right version |
| texture2DLod | textureLod | the same for every variant: Lod, Proj, Grad |
| gl_ModelViewProjectionMatrix, gl_Vertex, ftransform() | your own uniforms and attributes | the old fixed-function state is gone from core |
3.30 core → OpenGL 3.3+
1#version 330 core← 12layout(location = 0) in vec3 aPos;← 23layout(location = 1) in vec3 aNormal;4layout(location = 2) in vec2 aUV;5uniform mat4 uMVP;6uniform mat3 uNormalMat;7out vec3 vNormal;← 38out vec2 vUV;9void main() {10 vNormal = uNormalMat * aNormal;11 vUV = aUV;12 gl_Position = uMVP * vec4(aPos, 1.0);13}
- 1#version 330 core is OpenGL 3.3 with the core profile: everything deprecated before, like attribute, varying and gl_FragColor, is gone.
- 2layout(location = n) fixes the attribute's index, so glVertexAttribPointer(n, …) on the C++ side needs no lookup. Available from GLSL 3.30 and ES 3.00.
- 3out replaces varying in the vertex shader. The fragment shader declares the same name and type as in.
Pick a dialect and switch between the two stages. The numbered lines are the ones that change from dialect to dialect; the body of main() is the same in all five. "Compile" gives the exact text to the GLSL compiler inside this browser. WebGL 2 speaks only GLSL ES, so it accepts ES 1.00 and ES 3.00 and rejects the rest with its real error messages.
The difference you meet first is precision. A desktop GPU computes every float with 32 bits. A phone GPU may offer a faster 16-bit float, and GLSL ES lets the shader choose with highp, mediump and lowp, as the Types & Vectors chapter explains. The vertex shader has a default (highp), but the fragment shader has none for float, so a GLSL ES fragment shader without a precision line does not compile: "No precision specified for (float)".
1#version 300 es
2precision highp float; // required: a fragment shader has no default float precision
3precision highp sampler2D; // optional: samplers default to lowp, enough for 8-bit colour but not for float textures
4
5in vec2 vUV;
6uniform sampler2D uAlbedo;
7out vec4 FragColor;
8
9void main() {
10 FragColor = texture(uAlbedo, vUV);
11}GLSL ES is also stricter about types. Desktop GLSL from 1.20 on turns an int into a float where one is expected, so float x = 1; compiles there. GLSL ES never converts on its own: write 1.0. A shader developed only on a desktop compiler often breaks on a phone for exactly this reason.
| Missing in WebGL 2 (GLSL ES 3.00) | What to do instead |
|---|---|
| noperspective | the extension GL_NV_shader_noperspective_interpolation where it exists, or the two-varying trick from the Built-in Variables chapter |
| geometry and tessellation shaders | build the geometry on the CPU, or expand it in the vertex shader from gl_VertexID |
| compute shaders and storage buffers | a fragment shader drawing into a texture, with ping-pong between two textures; or WebGPU |
| double, dFdxFine, dFdxCoarse | float, and plain dFdx |
Vulkan does not accept shader text at all. It takes SPIR-V, a binary intermediate language, and the GLSL is compiled to SPIR-V ahead of time, usually as a build step. Two compilers ship with the Vulkan SDK: glslc (from Google's shaderc) and glslangValidator (the Khronos reference compiler). Both use the same front end and produce the same kind of file.
1glslc shader.vert -o shader.vert.spv # the stage comes from the extension: .vert .frag .comp ...
2glslc shader.frag -o shader.frag.spv
3glslc -O shader.frag -o shader.frag.spv # optimise the SPIR-V
4
5glslangValidator -V shader.vert -o shader.vert.spv # the reference compiler; -V = for Vulkan
6
7spirv-dis shader.vert.spv # print the SPIR-V as readable textThe language is GLSL 4.50 or 4.60 with a few changes, written down in the GL_KHR_vulkan_glsl extension. The compiler applies them because it is told to compile for Vulkan, not because the source asks for them. Most of the changes come from one fact: Vulkan has no glUniform calls. Everything a shader reads from outside comes from a descriptor set or from push constants.
| Topic | OpenGL GLSL | Vulkan GLSL |
|---|---|---|
| plain values | uniform mat4 uMVP; | only inside a uniform block; a loose float or mat4 is an error |
| where a resource comes from | a location or binding set from C++ with glUniform* or glBindBufferBase | layout(set = s, binding = b) |
| small per-draw values | a uniform | a layout(push_constant) block, at least 128 bytes |
| matching stages | by name; location optional | by location; layout(location = n) required on every in and out |
| vertex and instance number | gl_VertexID, gl_InstanceID | gl_VertexIndex, gl_InstanceIndex; the instance index includes firstInstance |
| textures | sampler2D | sampler2D, or a texture2D and a sampler kept apart and joined where used: sampler2D(tex, samp) |
| compile-time constants | #define N 4 | also specialization constants, set when the pipeline is created |
1#version 450
2
3// A specialization constant: 4 unless the pipeline sets constant 0 to something else.
4// The driver then compiles the shader with that value, as if it had been a #define.
5layout(constant_id = 0) const int SAMPLES = 4;
6
7// A texture and a sampler kept apart: one sampler can serve many textures
8layout(set = 0, binding = 0) uniform texture2D uColor;
9layout(set = 0, binding = 1) uniform sampler uLinear;
10
11layout(location = 0) in vec2 vUV;
12layout(location = 0) out vec4 FragColor;
13
14void main() {
15 vec4 sum = vec4(0.0);
16 for (int i = 0; i < SAMPLES; i++) {
17 vec2 offset = vec2(float(i) / float(SAMPLES) - 0.5, 0.0) * 0.01;
18 sum += texture(sampler2D(uColor, uLinear), vUV + offset); // joined where it is used
19 }
20 FragColor = sum / float(SAMPLES);
21}GLSL has a preprocessor much like C's, which runs before the compiler proper. Besides #define, #if, #ifdef and #else, it gives each dialect a few macros to test: __VERSION__ holds the version number (330, 300…), GL_ES is defined as 1 in GLSL ES, and VULKAN is defined as 100 when compiling for Vulkan.
The usual way to share a shader between targets is to leave the #version line out of the file and let the program put it in front when it loads the source, together with any settings. The body uses the macros to cover the differences that remain.
1// The file has no #version line; the program chooses it for the API it runs on.
2std::string header =
3#ifdef __EMSCRIPTEN__
4 "#version 300 es\n"; // a WebGL 2 build
5#else
6 "#version 330 core\n"; // desktop OpenGL
7#endif
8header += "#define MAX_LIGHTS 8\n"; // settings go in the same way
9
10std::string source = header + readFile("shaders/lit.frag");1#ifdef GL_ES
2precision highp float; // GLSL ES needs it (desktop 1.30+ would accept and ignore it)
3#endif
4
5#ifndef MAX_LIGHTS
6#define MAX_LIGHTS 4 // a default if the program did not set one
7#endif
8#if MAX_LIGHTS > 16
9#error MAX_LIGHTS is too large for the uniform array
10#endif
11
12#ifdef VULKAN
13layout(set = 0, binding = 0) uniform Lights { vec4 pos[MAX_LIGHTS]; } lights;
14#define LIGHT_POS(i) lights.pos[i]
15#else
16uniform vec4 uLightPos[MAX_LIGHTS];
17#define LIGHT_POS(i) uLightPos[i]
18#endif#extension
Features newer than the version, or offered only by some GPUs, are switched on by name with #extension, right after the #version line. require stops the compile if the extension is missing; enable turns it on if it is there, and the shader can test the macro of the same name to see whether it is. WebGL extensions with shader parts work the same way, once JavaScript has enabled them with getExtension.
1#version 300 es
2#extension GL_NV_shader_noperspective_interpolation : enable
3
4#ifdef GL_NV_shader_noperspective_interpolation
5noperspective in vec3 vEdgeDist; // the real qualifier, where the GPU has it
6#else
7in vec3 vEdgeDist; // fall back to perspective-correct
8#endif
9
10// Vulkan, bindless textures (see Modern Vulkan):
11// #extension GL_EXT_nonuniform_qualifier : require| Message or symptom | Cause | Fix |
|---|---|---|
| #version directive must occur on the first line | a line break or comment before #version, in WebGL | start the string with #version |
| No precision specified for (float) | a GLSL ES fragment shader without a precision line | precision highp float; |
| works on the PC, fails on a phone | float x = 1; and similar int-to-float conversions | write float literals: 1.0 |
| works on one desktop GPU, fails on another | some drivers accept code the specification forbids | check shaders with glslangValidator, which follows the specification |
| SPIR-V requires location for user input/output | an in or out without layout(location) in Vulkan | number every in and out |
| non-opaque uniforms outside a block | an OpenGL-style loose uniform compiled for Vulkan | move it into a uniform block or a push_constant block |
Key ideas
- #version picks the edition of the language: 330 = OpenGL 3.3, 300 es = WebGL 2, 450 with glslc = Vulkan.
- Old GLSL (attribute, varying, gl_FragColor, texture2D) ports to modern GLSL word for word.
- GLSL ES fragment shaders need a precision line, and GLSL ES never turns an int into a float for you.
- Vulkan GLSL is compiled to SPIR-V ahead of time; resources come from descriptor sets and push constants, and every in and out has a location.
- Leave #version out of shared files: the loader adds it, and the preprocessor (
GL_ES, VULKAN, __VERSION__) covers the rest.