GLSL não é uma linguagem só. É uma família de linguagens muito parecidas: uma para o OpenGL de desktop, uma para celulares e para a web (GLSL ES) e um conjunto de regras para o Vulkan. Elas compartilham quase todas as funções e tipos, mas um shader escrito para uma muitas vezes é recusado por outra. Este capítulo mostra como distinguir os dialetos, como levar um shader de um para outro e como manter um único código-fonte que compila em todos.
Ao final você vai conseguir
- Ler uma linha #version e dizer para qual API e quais GPUs ela serve.
- Portar um shader antigo que usa attribute, varying e gl_FragColor.
- Escrever um fragment shader que compila no WebGL 2 e em celulares.
- Transformar um shader de OpenGL num de Vulkan e compilá-lo para SPIR-V.
- Manter um só código de shader para vários alvos com o pré-processador.
A primeira coisa que um compilador de GLSL lê é a linha #version. Ela diz em qual edição da linguagem o resto do arquivo foi escrito, e o compilador passa a aceitar exatamente as palavras e as regras dessa edição. O número é a versão do GLSL vezes 100: #version 330 quer dizer GLSL 3.30.
As versões do GLSL de desktop acompanham o OpenGL. Até o OpenGL 3.2 os números eram diferentes (o OpenGL 2.0 veio com o GLSL 1.10), e do OpenGL 3.3 em diante eles coincidem: GLSL 3.30 é OpenGL 3.3, GLSL 4.60 é OpenGL 4.6. Um shader de desktop sem nenhuma linha #version é compilado como GLSL 1.10, a edição mais antiga, o que quase nunca é o que você quer.
| #version | API | O que trouxe |
|---|---|---|
| 110 | OpenGL 2.0 | o primeiro GLSL; também é o que você recebe sem linha #version |
| 120 | OpenGL 2.1 | matrizes não quadradas como mat2x3 |
| 130 | OpenGL 3.0 | in e out no lugar de attribute e varying, um texture() para todo tipo de sampler, inteiros completos |
| 140 | OpenGL 3.1 | uniform blocks |
| 150 | OpenGL 3.2 | geometry shaders, interface blocks, os perfis core e compatibility |
| 330 | OpenGL 3.3 | layout(location = n) nas entradas do vertex shader e nas saídas do fragment shader; os números passam a coincidir com o OpenGL |
| 400 | OpenGL 4.0 | tessellation shaders, double |
| 420 | OpenGL 4.2 | layout(binding = n), image load e store, atomic counters |
| 430 | OpenGL 4.3 | compute shaders, shader storage buffers, layout(location) em uniforms |
| 450 | OpenGL 4.5 | dFdxFine e dFdxCoarse, cull distances |
| 460 | OpenGL 4.6 | gl_BaseVertex, gl_BaseInstance e gl_DrawID embutidos; o OpenGL passa a carregar SPIR-V |
O GLSL ES é a edição do OpenGL ES, a API de celulares e tablets, e do WebGL, que é o OpenGL ES dentro do navegador. A linha de versão dele termina em es, menos a primeira:
| #version | API | O que trouxe |
|---|---|---|
| 100 (ou sem linha) | OpenGL ES 2.0, WebGL 1 | o estilo antigo: attribute, varying, gl_FragColor, texture2D |
| 300 es | OpenGL ES 3.0, WebGL 2 | o estilo moderno do 3.30 de desktop: in, out, texture(), layout(location) |
| 310 es | OpenGL ES 3.1 | compute shaders, storage buffers, image load e store; não existe no WebGL |
| 320 es | OpenGL ES 3.2 | geometry e tessellation shaders; não existe no WebGL |
Do 1.50 em diante, a linha #version de desktop pode nomear um perfil: core ou compatibility. O core contém só a linguagem moderna. O compatibility mantém também tudo o que foi depreciado, como gl_FragColor e gl_ModelViewMatrix, e só funciona com um contexto OpenGL de compatibilidade. Sem a palavra do perfil, 1.50 em diante quer dizer core. Esta trilha e a trilha de OpenGL sempre escrevem core.
Boa parte do GLSL na internet, e todo shader GLSL ES 1.00 escrito para o WebGL 1, ainda usa as palavras de antes do GLSL 1.30. Elas não estão erradas no dialeto delas, mas um shader #version 330 core ou #version 300 es as recusa. Portar é mecânico:
| Antigo (1.10, 1.20, ES 1.00) | Moderno (3.30, ES 3.00, Vulkan) | Nota |
|---|---|---|
| attribute vec3 aPos; | layout(location = 0) in vec3 aPos; | entradas do vertex shader |
| varying vec2 vUV; | out vec2 vUV; (vertex) · in vec2 vUV; (fragment) | uma palavra virou duas, uma de cada lado |
| gl_FragColor = c; | out vec4 FragColor; … FragColor = c; | você declara a saída e escolhe o nome dela |
| gl_FragData[1] = n; | layout(location = 1) out vec4 Normal; | vários render targets, como no deferred shading |
| texture2D, textureCube, shadow2D | texture | o tipo do sampler escolhe a versão certa |
| texture2DLod | textureLod | o mesmo para toda variante: Lod, Proj, Grad |
| gl_ModelViewProjectionMatrix, gl_Vertex, ftransform() | seus próprios uniforms e atributos | o antigo estado de função fixa não existe mais no 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 é o OpenGL 3.3 com o perfil core: tudo o que foi depreciado antes, como attribute, varying e gl_FragColor, deixa de existir.
- 2layout(location = n) fixa o índice do atributo, então o glVertexAttribPointer(n, …) do C++ não precisa perguntar. Disponível a partir do GLSL 3.30 e do ES 3.00.
- 3out substitui varying no vertex shader. O fragment shader declara o mesmo nome e tipo como in.
Escolha um dialeto e alterne entre os dois estágios. As linhas numeradas são as que mudam de um dialeto para outro; o corpo do main() é o mesmo nos cinco. "Compilar" entrega o texto exato ao compilador de GLSL que existe dentro deste navegador. O WebGL 2 só fala GLSL ES, então aceita o ES 1.00 e o ES 3.00 e recusa os outros com as mensagens de erro reais.
A primeira diferença que você encontra é a precisão. Uma GPU de desktop calcula todo float com 32 bits. Uma GPU de celular pode oferecer um float de 16 bits mais rápido, e o GLSL ES deixa o shader escolher com highp, mediump e lowp, como explica o capítulo Tipos e vetores. O vertex shader tem um padrão (highp), mas o fragment shader não tem nenhum para float, então um fragment shader GLSL ES sem linha de precisão não compila: "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}O GLSL ES também é mais rígido com os tipos. O GLSL de desktop, do 1.20 em diante, transforma um int em float onde um float é esperado, então float x = 1; compila ali. O GLSL ES nunca converte sozinho: escreva 1.0. Um shader desenvolvido só num compilador de desktop muitas vezes quebra no celular exatamente por isso.
| Ausente no WebGL 2 (GLSL ES 3.00) | O que fazer no lugar |
|---|---|
| noperspective | a extensão GL_NV_shader_noperspective_interpolation onde ela existe, ou o truque das duas varyings do capítulo Variáveis embutidas |
| geometry e tessellation shaders | monte a geometria na CPU, ou expanda-a no vertex shader a partir de gl_VertexID |
| compute shaders e storage buffers | um fragment shader desenhando numa textura, com ping-pong entre duas texturas; ou WebGPU |
| double, dFdxFine, dFdxCoarse | float, e o dFdx simples |
O Vulkan não aceita texto de shader. Ele recebe SPIR-V, uma linguagem intermediária binária, e o GLSL é compilado para SPIR-V antes, normalmente como uma etapa do build. Dois compiladores vêm com o Vulkan SDK: o glslc (do shaderc, do Google) e o glslangValidator (o compilador de referência da Khronos). Os dois usam o mesmo front end e produzem o mesmo tipo de arquivo.
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 textA linguagem é o GLSL 4.50 ou 4.60 com algumas mudanças, descritas na extensão GL_KHR_vulkan_glsl. O compilador as aplica porque recebeu a ordem de compilar para Vulkan, não porque o código pede. A maior parte das mudanças vem de um fato só: o Vulkan não tem chamadas glUniform. Tudo o que um shader lê de fora vem de um descriptor set ou de push constants.
| Assunto | GLSL do OpenGL | GLSL do Vulkan |
|---|---|---|
| valores simples | uniform mat4 uMVP; | só dentro de um uniform block; um float ou mat4 solto é erro |
| de onde vem um recurso | um location ou binding definido no C++ com glUniform* ou glBindBufferBase | layout(set = s, binding = b) |
| valores pequenos por draw | um uniform | um bloco layout(push_constant), de pelo menos 128 bytes |
| ligação entre estágios | pelo nome; location opcional | pelo location; layout(location = n) obrigatório em todo in e out |
| número do vértice e da instância | gl_VertexID, gl_InstanceID | gl_VertexIndex, gl_InstanceIndex; o índice da instância inclui o firstInstance |
| texturas | sampler2D | sampler2D, ou um texture2D e um sampler separados e juntados onde são usados: sampler2D(tex, samp) |
| constantes de compilação | #define N 4 | também specialization constants, definidas quando o pipeline é criado |
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}O GLSL tem um pré-processador muito parecido com o do C, que roda antes do compilador propriamente dito. Além de #define, #if, #ifdef e #else, ele dá a cada dialeto algumas macros para testar: __VERSION__ guarda o número da versão (330, 300…), GL_ES é definida como 1 no GLSL ES, e VULKAN é definida como 100 ao compilar para Vulkan.
O jeito comum de compartilhar um shader entre alvos é deixar a linha #version fora do arquivo e deixar o programa colocá-la na frente ao carregar o código, junto com as configurações. O corpo usa as macros para cobrir as diferenças que sobram.
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
Recursos mais novos que a versão, ou oferecidos só por algumas GPUs, são ligados pelo nome com #extension, logo depois da linha #version. require interrompe a compilação se a extensão não existir; enable a liga se ela estiver lá, e o shader pode testar a macro de mesmo nome para saber se está. Extensões do WebGL que têm parte no shader funcionam do mesmo jeito, depois que o JavaScript as habilitou com 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| Mensagem ou sintoma | Causa | Correção |
|---|---|---|
| #version directive must occur on the first line | uma quebra de linha ou comentário antes do #version, no WebGL | comece a string com #version |
| No precision specified for (float) | um fragment shader GLSL ES sem linha de precisão | precision highp float; |
| funciona no PC, falha no celular | float x = 1; e conversões parecidas de int para float | escreva literais float: 1.0 |
| funciona numa GPU de desktop, falha em outra | alguns drivers aceitam código que a especificação proíbe | verifique os shaders com o glslangValidator, que segue a especificação |
| SPIR-V requires location for user input/output | um in ou out sem layout(location) no Vulkan | numere todo in e out |
| non-opaque uniforms outside a block | um uniform solto no estilo do OpenGL compilado para Vulkan | mova-o para um uniform block ou um bloco push_constant |
Ideias-chave
- #version escolhe a edição da linguagem: 330 = OpenGL 3.3, 300 es = WebGL 2, 450 com glslc = Vulkan.
- O GLSL antigo (attribute, varying, gl_FragColor, texture2D) vira GLSL moderno palavra por palavra.
- Fragment shaders GLSL ES precisam de uma linha de precisão, e o GLSL ES nunca transforma um int em float por você.
- O GLSL do Vulkan é compilado para SPIR-V antes; os recursos vêm de descriptor sets e push constants, e todo in e out tem um location.
- Deixe o #version fora dos arquivos compartilhados: o carregador o adiciona, e o pré-processador (
GL_ES, VULKAN, __VERSION__) cobre o resto.