Les shaders ou nuanceur dans la terre sacré du Québec sont des programmes destinés à paramétrer certains
comportements de la pipeline graphique dynamique...
Pas très clair ?
Pas très grave ! On ne manipulera que deux types de shaders dans cette exemple de webGL et n'avons donc qu'à
présenter ces deux types.
Sûrement le plus simple à appréhender. Le nuanceur de fragment sert à définir la couleur de sortie d'un pixel
sur l'écran en
fonction de paramètres passés en entré comme sa position sur l'écran, la direction de la lumière, l'orientation
de la caméra...
Une bonne manière de se les représenter est de penser qu'ils fonctionnent comme des équations cartésiennes.
Par exemple pour un disque de centre (x0, y0) et de rayon R:
(D) : (x - x0)2 + (y
- y0)2 ≤ R2
Pour tracer ce disque, on prend un point sur le plan et regarde si il vérifie bien l'inéquation (D), si c'est
le cas on peut le colorier.
Ceci se traduit presque automatique par ce frag shader :
#define NOIR = vec3(0.0, 0.0, 0.0)
#define JAUNE vec3(1.0, 1.0, 0.0)
bool est_dans_disque(float x, float y, float x0, float y0, float r) {
float xs = x - x0;
float ys = y - y0;
return xs*xs + ys*ys <= r * r;
}
void main() {
//la coordonée du pixel
vec2 point = gl_FragCoord.xy;
vec3 colour NOIR;
if (est_dans_disque(point.x, point.y, 0.0, 200.0, 200.0)) {
colour = JAUNE;
}
//gl_FragColor c'est la couleur que l'on donne à notre pixel
gl_FragColor = vec4(colour, 1.0);
}
On peut avec ça tracer à peu près n'importe quelle figure mathématique. Pour savoir tout ce qu'il est possible de faire avec seulement les nuanceurs de fragments je vous invite à visiter Shadertoy.
Les nuanceurs de sommets serve à optimiser le rendu gpu.
Une petite mise en contexte :
Vous êtes développeur d'un jeu vidéo 3D et votre jeu affiche des milliers d'objets 3D tous composés en moyenne
d'une centaine de sommets.
Cependant vous n'en afficher à la caméra guère plus que dix.
Comment vérifier si l'un des milles objets doit être affiché, c'est à dire comment vérifier qu'il ne dépasse
pas du champ
de vision du joueur et qu'il n'est pas caché derrière un autre objet ? Pire, comment faire tous ça en un
soixantième de seconde pour des milliers d'objets composés de centaines de sommets ?
Les nuanceurs de sommets arrivent à la rescousse ! Ces shaders sont utilisés pour déterminer la position des
sommets des objets par rapport
à la caméra et faire « clipper » ce qui dépassent ou ce qui dont derrière un autre objet. Ainsi ne sont
calculées
pour les pixels que les valeurs
réellement utiles.
Pour ce projet WebGl on n'utilisera à peine les vertex shader car on utilise une technique de rendu spéciale,
le
marchage de rayon.
Dans cet l'exemple on ne fait que transmettre la position de nos sommets car on rend une image 2D mais on pourrais
appliquer des transformations comme des translatons ou des rotations par rapport au joueur si on était dans le cas d'un rendu 3D.
// Pour openGL la zone affichable s'étend dans le carré unité:
// ((-1, -1), (-1, 1), (1, 1), (1, -1)).
// La position du sommet pour lequel on veut savoir
// si il doit être affiché ou non.
attribute vec2 a_position;
void main() {
// On passe simplement la position de notre vertex à GL et on prendra soin de
// vérifier qu'elle est dans [-1; -1] × [-1; -1]
gl_Position = vec4(a_position, 0.0, 1.0);
}
Une fois qu'on sait ce qui doit être affiché (les vertex shaders) et comment on veut l'afficher (les fragment shaders) ça serait
intéressant de les combiner pour par exemple rendre un objet avec un matériel solide et l'autre réfléchissant ?
On utilise pour cela un shader program(me), qui est une union de plusieurs shaders utilisé pour le rendu d'un même objet.
Dans notre cas on n'utilisera qu'un seul programme composé d'un seul nuanceur de sommets et d'un seul nuanceur de fragments.
Avec tous ce qui a été dit on peut désormais comprendre la suite d'intruction webGl suivante :
/*
* Crée le programme à l'aide des sources d'un fragment et d'un vertex shader.
*/
function makeProgram(vertexShaderSource, fragmentShaderSource) {
const vertexShader = makeShader(vertexShaderSource, gl.VERTEX_SHADER);
const fragmentShader = makeShader(fragmentShaderSource, gl.FRAGMENT_SHADER);
const programId = gl.createProgram();
gl.attachShader(programId, vertexShader);
gl.attachShader(programId, fragmentShader);
checkShader(vertexShader);
checkShader(fragmentShader);
gl.linkProgram(programId);
return programId;
}
/**
* Crée un shader à partir de :
* - une source (une chaîne de caractère)
* - un type (gl.VERTEX_SHADER ou gl.FRAGMENT_SHADER)
*/
function makeShader(shaderSource, shaderType) {
const shader = gl.createShader(shaderType);
gl.shaderSource(shader, shaderSource);
gl.compileShader(shader);
return shader;
}
/**
* Vérifie que la création du shader a réussie
*/
function checkShader(shader) {
const log = gl.getShaderInfoLog(shader);
if (log) {
throw log;
}
}
const VERTEX_SHADER = `
// Pour openGL la zone affichable s'étend dans le carré unité:
// ((-1, -1), (-1, 1), (1, 1), (1, -1)).
// La position du sommet pour lequel on veut savoir
// si il doit être affiché ou non.
attribute vec2 a_position;
void main() {
// On passe simplement la position de notre vertex à GL et on prendra soin de
// vérifier qu'elle est dans [-1; -1] × [-1; -1]
gl_Position = vec4(a_position, 0.0, 1.0);
}
`;
const FRAGMENT_SHADER = `
#define NOIR = vec3(0.0, 0.0, 0.0)
#define JAUNE vec3(1.0, 1.0, 0.0)
bool est_dans_cercle(float x, float y, float x0, float y0, float r) {
float xs = x - x0;
float ys = y - y0;
return xs*xs + ys*ys <= r * r;
}
void main() {
//la coordonée du pixel
vec2 point = gl_FragCoord.xy;
vec3 colour NOIR;
if (est_dans_cercle(point.x, point.y, 0.0, 200.0, 200.0)) {
colour = JAUNE;
}
//gl_FragColor c'est la couleur que l'on donne à notre pixel
gl_FragColor = vec4(colour, 1.0);
}
`;
function start() {
// [...] ici le code qui initialise la variable « vertexBuffer »
//dit à gl d'utiliser notre programme
const programId = makeProgram(VERTEX_SOURCE, FRAGMENT_SOURCE);
//Pour savoir ou le vertex buffer doit mettre ses sommets
const aPositionLocation = gl.getAttribLocation(programId, "a_position");
// [...] Le reste concerne le copiage du vertexBuffer à aPositionLocation
// qui est expliqué dans le chapitre « les tampons »
}
// [...]
function render() {
// [...]
// Charge le programme pour le rendu
gl.useProgram(programId);
// Enfin on dessine notre carré !
gl.drawArrays(gl.TRIANGLES,0, 6);
}