gKit2 light
openGL 4.3 : storage textures / images

cf tuto_storage_texture.cpp

compute shader et textures

Les textures sont accessibles en lecture avec un sampler, comme dans les fragment shaders. Il y a quand même une différence, habituellement le pipeline graphique calcule automatiquement les paramètres de filtrage de la texture, mais comme un compute shader ne fait pas parti du pipeline graphique, il faut choisir le mipmap explicitement avec textureLod(), par exemple.

écrire dans une texture

Lorsqu'un shader (fragment ou compute) veut écrire dans une texture, il faut "emballer" la texture dans un objet openGL, une texture image (rappel: pour que le pipeline graphique écrive dans une texture, il faut aussi l'emballer dans un framebuffer object, cf rendu multi-passes : shadow maps, post process, framebuffer object). Et comme d'habitude, plusieurs images peuvent être déclarées, elles sont donc numérotées, comme les unités de textures, ou les sorties d'un framebuffer object.

C'est glBindImageTexture() qui permet de configurer le pipeline / d'associer une texture à une unité :

GLuint texture= { ... };
GLint unit= 0;
glBindImageTexture( unit, /* texture */ texture, /* level */ 0,
/* layered */ GL_TRUE, /* layer */ 0,
/* access */ GL_WRITE_ONLY, /* format */ GL_RGBA8 );

Pour des textures 2d classiques, les paramètres layered et layer sont ignorés. Il faut donc préciser la texture, le niveau de mipmap à sélectionner, le type d'accès (lecture seule GL_READ_ONLY, écriture seule GL_WRITE_ONLY, mixte GL_READ_WRITE) et le format interne des texels (GL_RGBA8 pour les textures classiques avec des valeurs 0..255 par canal et 4 canaux, rgb et a) à utiliser lorsque le shader écrit une valeur dans l'image.

Dernière étape, fournir au shader, le numéro de l'unité sur laquelle la texture est sélectionnée, c'est le même principe que pour les unités de textures et les samplers :

// selectionner le shader program, si necessaire
// glUseProgram(program);
GLint location= glGetUniformLocation(program, "...");
glUniform1i(location, unit);

déclaration dans le shader

Les images sont des uniforms déclarés avec le type :

La déclaration doit aussi être décorée avec le format des données en cas de lecture (ou d'accès mixte) et éventuellement l'indice de l'unité sur laquelle la texture est sélectionnée :

layout(binding= 0) writeonly uniform image2D image;
layout(binding= 0, rgba8) readonly uniform image2D image;
layout(binding= 0, rgba8) coherent uniform image2D image;

attention : les accès déclarés dans le shader et par l'application (cf glBindImageTexture()) doivent être les mêmes.

Tous les formats de textures ne sont pas disponibles, seuls les versions 1, 2 et 4 canaux existent : r, rg, rgba, par contre les formats sont les memes, 8bits entiers, 16bits réels, 32bits réels et entiers, signés ou pas. Ils sont désignés par les suffixes : 8, 16 ou 32 pour la taille et par i, ui ou f pour entier, entier non signé et réel. Voici les types courants : rgba8, rgba32f, r32ui.

lecture

c'est la fonction imageLoad( image, ivec2 pixel ) qui permet de lire une valeur. Les coordonnées du pixel sont comprises entre 0 et largeur / hauteur. La valeur renvoyée dépend de la déclaration de l'image. image2D renvoie un vec4, uimage2D renvoie un uvec4, etc.

L'image doit etre déclarée en lecture :

// shader
layout(binding= 0, rgba8) readonly uniform image2D image;
void main( )
{
vec4 color= imageLoad(image, ivec2(x, y));
...
}
// application
GLuint program= { ... };
GLuint texture= { ... };
GLint unit= 0;
glBindImageTexture( unit, texture, /* level*/ 0,
/* layered */ GL_TRUE, /* layer */ 0,
/* access */ GL_READ_ONLY, /* format*/ GL_RGBA8 );
GLint location= glGetUniformLocation(program, "image");
glUniform1i(location, unit);
vecteur generique 4d, ou 3d homogene, utilitaire.
Definition: vec.h:168

écriture

c'est la fonction imageStore( image, ivec2 pixel, values ) qui permet d'écrire une valeur. le type des valeurs écrites dans l'image dépend aussi de la déclaration de l'image. Et bien sur l'image doit etre déclarée en écriture :

// shader
layout(binding= 0) writeonly uniform image2D image;
void main( )
{
imageStore(image, ivec2(x, y), vec4(r, g, b, a));
...
}
// application
GLuint program= { ... };
GLuint texture= { ... };
GLint unit= 0;
glBindImageTexture( unit, texture, /* level*/ 0,
/* layered */ GL_TRUE, /* layer */ 0,
/* access */ GL_WRITE_ONLY, /* format*/ GL_RGBA8 );
GLint location= glGetUniformLocation(program, "image");
glUniform1i(location, unit);

opération atomiques

les images permettent aussi d'utiliser les opérations atomiques imageAtomicAdd(), imageAtomicMin(), imageAtomicMax(), imageAtomicAnd(), imageAtomicOr(), imageAtomicXor() et imageAtomicExchange()imageAtomicCompSwap().

L'image doit être déclarée en lecture / écriture cohérente et avec un type entier 32 bits :

// shader
layout(binding= 0, r32ui) coherent uniform uimage2D image;
void main( )
{
imageAtomicAdd(image, ivec2(x, y), 1);
...
}
// application
GLuint program= { ... };
GLuint texture= { ... };
GLint unit= 0;
glBindImageTexture( unit, texture, /* level*/ 0,
/* layered */ GL_TRUE, /* layer */ 0,
/* access */ GL_READ_WRITE, /* format*/ GL_R32UI );
GLint location= glGetUniformLocation(program, "image");
glUniform1i(location, unit);

exemples

voici quelques exemples de création, configuration et de shader :

texture avec des pixels de type 1 float

1 canal float, format de texture GL_R32F / GL_RED :

application :

// creation
GLenum texel_format= GL_R32F;
GLenum data_format= GL_RED;
GLenum data_type= GL_FLOAT;
GLint texture= 0;
glGenTexures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, /* mipmap level */ 0, texel_format, width, height, 0,
data_format, data_type, nullptr);
// pas de mipmaps
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
// configuration
GLint unit= 0;
glBindImageTexture(unit, texture, /* mipmap level */ 0,
/* layered */ GL_TRUE, /* layer */ 0,
/* access */ GL_READ_WRITE, /* texel format */ texel_format );

les autres acces sont GL_READ_ONLY (que lecture), GL_WRITE_ONLY (qu'ecriture), GL_READ_WRITE (lecture / ecriture). à modifier en fonction de ce que fait le shader (uniquement imageLoad(), uniquement imageStore(), ou les 2...)

shader : déclaration image2D, texels de type float

layout(binding= ..., r32f) coherent image2D image;
// on peut remplacer coherent (acces en lecture / ecriture) par readonly, ou writeonly, en fonction de l'utilisation de la texture,
// cf imageLoad(), imageStore();
// ne pas oublier de changer le parametre du glBindImageTexture() dans l'application...
imageStore(image, ivec2(x, y), vec4(r, 0,0,0));
// imageStore prend toujours un vec4() en parametre. la texture ne contient qu'un seul canal, vec4(r, 0, 0, 0);
vec4 pixel= imageLoad(image, ivec2(x, y));
// imageLoad renvoie toujours un vec4. la texture ne contient qu'un seul canal, cf pixel.r
float color= pixel.r;

texture avec des pixels de type 2 ou 4 floats

2 canaux float, format de texture GL_RG32F / GL_RG :

4 canaux float, format de texture GL_RGBA32F / GL_RGBA :

application :

// creation
GLenum texel_format= GL_RG32F; // ou GL_RGBA32F pour 4 canaux float
GLenum data_format= GL_RG; // ou GL_RGBA pour 4 canaux
GLenum data_type= GL_FLOAT;
GLint texture= 0;
glGenTexures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, /* mipmap level */ 0, texel_format, width, height, 0,
data_format, data_type, nullptr);
// pas de mipmaps
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
// configuration
GLint unit= 0;
glBindImageTexture(unit, texture, /* mipmap level */ 0,
/* layered */ GL_TRUE, /* layer */ 0,
/* access */ GL_READ_WRITE, /* texel format */ texel_format );

shader :

layout(binding= ..., rg32f) coherent image2D image;
// rg32f pour 2 canaux, ou rgba32f pour 4 canaux
// on peut remplacer coherent (acces en lecture / ecriture) par readonly, ou writeonly, en fonction de l'utilisation de la texture,
// cf imageLoad(), imageStore();
imageStore(image, ivec2(x, y), vec4(r, g, 0,0));
// imageStore prend toujours un vec4() en parametre. la texture ne contient que 2 canaux, vec4(r, g, 0, 0);
// si la texture contient 4 canaux float
// imageStore(image, ivec2(x, y), vec4(r, g, b, a));
vec4 pixel= imageLoad(image, ivec2(x, y));
// imageLoad renvoie toujours un vec4. la texture contient 2 canaux, cf pixel.rg
vec2 color= pixel.rg;
// ou vec4 color= pixel; si la texture contient 4 canaux
vecteur generique, utilitaire.
Definition: vec.h:131

texture avec des pixels de type 1 uint / unsigned int

application :

// creation
GLenum texel_format= GL_R32UI;
GLenum data_format= GL_RED_INTEGER;
GLenum data_type= GL_UNSIGNED_INTEGER;
GLint texture= 0;
glGenTexures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, /* mipmap level */ 0, texel_format, width, height, 0,
data_format, data_type, nullptr);
// pas de mipmaps
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
// configuration
GLint unit= 0;
glBindImageTexture(unit, texture, /* mipmap level */ 0,
/* layered */ GL_TRUE, /* layer */ 0,
/* access */ GL_READ_WRITE, /* texel format */ texel_format );

shader : déclaration uimage2D, pour des texels uint

layout(binding= ..., r32ui) coherent uimage2D image;
imageStore(image, ivec2(x, y), uvec4(r, 0,0,0));
// imageStore prend toujours un uvec4() en parametre. la texture ne contient qu'un seul canal uint, uvec4(r, 0, 0, 0);
uvec4 pixel= imageLoad(image, ivec2(x, y));
// imageLoad renvoie toujours un uvec4. la texture ne contient qu'un seul canal, cf pixel.r
uint color= pixel.r;

texture avec des pixels de type 2 ou 4 uint

2 canaux uint, format de texture GL_RG32UI / GL_RG_INTEGER :

4 canaux uint, format de texture GL_RGBA32UI / GL_RGBA_INTEGER :

application :

// creation
GLenum texel_format= GL_RG32UI; // ou GL_RGBA32UI pour 4 canaux
GLenum data_format= GL_RG_INTEGER; // ou GL_RGBA_INTEGER pour 4 canaux
GLenum data_type= GL_UNSIGNED_INTEGER;
GLint texture= 0;
glGenTexures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, /* mipmap level */ 0, texel_format, width, height, 0,
data_format, data_type, nullptr);
// pas de mipmaps
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
// configuration
GLint unit= 0;
glBindImageTexture(unit, texture, /* mipmap level */ 0,
/* layered */ GL_TRUE, /* layer */ 0,
/* access */ GL_READ_WRITE, /* texel format */ texel_format );

shader : déclaration uimage2D, pour des texels uint.

layout(binding= ..., rg32ui) coherent uimage2D image;
// rg32ui pour 2 canaux uint, ou rgba32ui pour 4 canaux uint
imageStore(image, ivec2(x, y), uvec4(r, g, 0,0));
// imageStore prend toujours un uvec4() en parametre.
// si la texture contient 4 canaux uint
// imageStore(image, ivec2(x, y), uvec4(r, g, b, a));
uvec4 pixel= imageLoad(image, ivec2(x, y));
// imageLoad renvoie toujours un uvec4.
uvec2 color= pixel.rg;

texture avec des pixels de type int

application :

// creation
GLenum texel_format= GL_R32I; // ou GL_RGBA32I pour 4 canaux, GL_RG32I pour 2 canaux
GLenum data_format= GL_RED_INTEGER; // ou GL_RGBA_INTEGER pour 4 canaux, GL_RG_INTEGER pour 2 canaux
GLenum data_type= GL_INTEGER;
GLint texture= 0;
glGenTexures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexImage2D(GL_TEXTURE_2D, /* mipmap level */ 0, texel_format, width, height, 0,
data_format, data_type, nullptr);
// pas de mipmaps
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
// configuration
GLint unit= 0;
glBindImageTexture(unit, texture, /* mipmap level */ 0,
/* layered */ GL_TRUE, /* layer */ 0,
/* access */ GL_READ_WRITE, /* texel format */ texel_format );

shader : déclaration iimage2D, pour des texels int.

layout(binding= ..., r32i) coherent iimage2D image;
// rg32i pour 2 canaux int, ou rgba32i pour 4 canaux int
imageStore(image, ivec2(x, y), ivec4(r, g, 0,0));
// imageStore prend toujours un ivec4() en parametre.
// si la texture contient 4 canaux int
// imageStore(image, ivec2(x, y), ivec4(r, g, b, a));
ivec4 pixel= imageLoad(image, ivec2(x, y));
// imageLoad renvoie toujours un ivec4.
ivec2 color= pixel.r;

glTexImage2D() texel_format, data_format, quoi ?

glTexImage2D fait 2 choses différentes, créer la texture et initialiser son contenu et ses paramètres sont plus ou moins redondants, et il est assez simple de se tromper. pourquoi ? tout simplement parce que le driver converti les données passées en paramètres vers le format interne / le format des pixels et les paramètres data_format, data_type permettent d'interpreter correctement les données pour appliquer la bonne conversion.

openGL 4.3 a introduit une nouvelle api pour créer des textures : glTexStorage2D() qui ne fait que créer / allouer la texture. et glTexSubImage2D permet d'initialiser le contenu de la texture (c'est possible aussi avec openGL 3.3).

on peut écrire un code de création de texture plus simple avec openGL 4.3 :

// creation
GLenum texel_format= GL_RGBA;
GLint texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexStorage2D(GL_TEXTURE_2D, /* mipmap levels */ 1, /* format */ texel_format, width, height);
// glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0); est implicite

cette fonction crée directement tous les niveaux de mipmap. pas la peine de le fournir avec un glTexParameteri().

si l'on souhaite transférer des données, pour initialiser les niveaux de mipmap, on utilise ensuite :

// glBindTexture(GL_TEXTURE_2D, texture);
// si necessaire
glTexSubImage2D(GL_TEXTURE_2D, /* mipmap level */ 0,
/* rectangle a initialiser : x, y, w, h */ 0, 0, width, height,
/* data format */, /* data type */, /* data */ ... );

openGL 4.4 : initialiser une texture avec une couleur / valeur constante

des textures sont souvent utlisées pour stocker des infos ou des résultats de compute shaders, et il faut régulièrement re-initialiser le contenu d'une texture, et assez souvent, on souhaite uniquement une valeur constante pour tous les pixels. openGL 3.3 oblige à faire un peu de gynmastique : il faut creer un tableau de pixels de la bonne taille et utiliser glTexSubImage2D().

remarque : et oui, c'est lent, puisqu'il faut transférer tous les pixels de la texture vers la mémoire du gpu.

openGL 4.4 à introduit glClearTexImage() pour cette initialisation par une valeur constante. c'est plus simple et plus rapide :

// initialiser tous les pixels de la texture a 1
float color[4]= { 1, 1, 1, 1 };
glClearTexImage(texture, /* mipmap level */ 0, /* data format */ GL_RGBA, /* data type */ GL_FLOAT, /* data */ color);

les paramètres data_format, data_type sont les memes que ceux de glTexImage2D(), bien sur (les conversions sont les memes)...

cf tuto_storage_texture.cpp pour un exemple sur une texture GL_R32UI...