I. Prérequis▲
Premièrement, nous avons besoin d'inclure tous les fichiers d'en-têtes et faire les choses que nous faisons toujours, comme dans presque tous les autres tutoriels :
#include
<irrlicht.h>
#include
<iostream>
#include
"driverChoice.h"
using
namespace
irr;
#ifdef _MSC_VER
#pragma comment(lib,
"Irrlicht.lib"
)
#endif
II. Préparation des shaders▲
Parce que nous voulons utiliser quelques intéressants shaders dans ce tutoriel, nous avons besoin de définir quelques données afin qu'il puisse calculer de jolies couleurs. Dans cet exemple, nous utiliserons un simple vertex shader qui calculera la couleur du vertex en se basant sur la position de la caméra. Pour cela, le shader a besoin des données suivantes : l'inverse de la matrice du monde pour transformer la normale, la matrice de modèle-vue-projection pour transformer la position, la position de la caméra et la position de l'objet dans le monde pour calculer l'angle et la couleur de la lumière. Pour pouvoir indiquer au shader toutes ces données à chaque image, nous devons hériter de l'interface IShaderConstantSetCallBack et surcharger son unique méthode OnSetConstants(). Cette méthode sera appelée à chaque fois que le matériel est utilisé. La méthode setVertexShaderConstant() de l'interface ImaterialRendererServices est utilisée pour définir les données nécessaires au shader. Si l'utilisateur choisit un langage de shader haut niveau comme le HLSL à la place de l'assembleur dans cet exemple, vous devez définir le nom de la variable comme paramètre au lieu de l'index du registre.
IrrlichtDevice*
device =
0
;
bool
UseHighLevelShaders =
false
;
bool
UseCgShaders =
false
;
class
MyShaderCallBack : public
video::
IShaderConstantSetCallBack
{
public
:
virtual
void
OnSetConstants(video::
IMaterialRendererServices*
services,
s32 userData)
{
video::
IVideoDriver*
driver =
services->
getVideoDriver();
// Définit l'inverse de la matrice du monde
// si nous utilisons des shaders haut-niveau (que l'utilisateur peut sélectionner quand
// le programme démarre), nous devons définir les constantes selon leurs noms.
core::
matrix4 invWorld =
driver->
getTransform(video::
ETS_WORLD);
invWorld.makeInverse();
if
(UseHighLevelShaders)
services->
setVertexShaderConstant("mInvWorld"
, invWorld.pointer(), 16
);
else
services->
setVertexShaderConstant(invWorld.pointer(), 0
, 4
);
// Définit la matrice de modèle vue projection.
core::
matrix4 worldViewProj;
worldViewProj =
driver->
getTransform(video::
ETS_PROJECTION);
worldViewProj *=
driver->
getTransform(video::
ETS_VIEW);
worldViewProj *=
driver->
getTransform(video::
ETS_WORLD);
if
(UseHighLevelShaders)
services->
setVertexShaderConstant("mWorldViewProj"
, worldViewProj.pointer(), 16
);
else
services->
setVertexShaderConstant(worldViewProj.pointer(), 4
, 4
);
// Définit la position de la caméra.
core::
vector3df pos =
device->
getSceneManager()->
getActiveCamera()->
getAbsolutePosition();
if
(UseHighLevelShaders)
services->
setVertexShaderConstant("mLightPos"
, reinterpret_cast
<
f32*>
(&
pos), 3
);
else
services->
setVertexShaderConstant(reinterpret_cast
<
f32*>
(&
pos), 8
, 1
);
// Définit la couleur de la lumière.
video::
SColorf col(0.0
f,1.0
f,1.0
f,0.0
f);
if
(UseHighLevelShaders)
services->
setVertexShaderConstant("mLightColor"
,
reinterpret_cast
<
f32*>
(&
col), 4
);
else
services->
setVertexShaderConstant(reinterpret_cast
<
f32*>
(&
col), 9
, 1
);
// Définit la transposée de la matrice du monde.
core::
matrix4 world =
driver->
getTransform(video::
ETS_WORLD);
world =
world.getTransposed();
if
(UseHighLevelShaders)
{
services->
setVertexShaderConstant("mTransWorld"
, world.pointer(), 16
);
// Définit les textures, pour les textures, vous pouvez aussi bien utiliser l'interface int que l'interface float de setPixelShaderConstant (vous en avez besoin uniquement pour les pilotes OpenGL).
s32 TextureLayerID =
0
;
if
(UseHighLevelShaders)
services->
setPixelShaderConstant("myTexture"
, &
TextureLayerID, 1
);
}
else
services->
setVertexShaderConstant(world.pointer(), 10
, 4
);
}
}
;
Dans les lignes suivantes nous démarrons le moteur tout comme dans une grande partie des tutoriels précédents. Mais nous demandons en plus à l'utilisateur s'il veut utiliser des shaders haut niveau dans cet exemple, s'il a sélectionné un pilote capable de le faire.
int
main()
{
// Demande un pilote à l'utilisateur.
video::
E_DRIVER_TYPE driverType=
driverChoiceConsole();
if
(driverType==
video::
EDT_COUNT)
return
1
;
// Demande à l'utilisateur si on doit utiliser des shaders haut-niveau dans cet exemple.
if
(driverType ==
video::
EDT_DIRECT3D9 ||
driverType ==
video::
EDT_OPENGL)
{
char
i;
printf("Please press 'y' if you want to use high level shaders.
\n
"
);
std::
cin >>
i;
if
(i ==
'y'
)
{
UseHighLevelShaders =
true
;
printf("Please press 'y' if you want to use Cg shaders.
\n
"
);
std::
cin >>
i;
if
(i ==
'y'
)
UseCgShaders =
true
;
}
}
// Crée le moteur.
device =
createDevice(driverType, core::
dimension2d<
u32>
(640
, 480
));
if
(device ==
0
)
return
1
; // Ne peut pas créer le pilote sélectionné.
video::
IVideoDriver*
driver =
device->
getVideoDriver();
scene::
ISceneManager*
smgr =
device->
getSceneManager();
gui::
IGUIEnvironment*
gui =
device->
getGUIEnvironment();
// On s'assure que nous n'essayons pas Cg sans le support requit.
if
(UseCgShaders &&
!
driver->
queryFeature(video::
EVDF_CG))
{
printf("Warning: No Cg support, disabling.
\n
"
);
UseCgShaders=
false
;
}
III. Chargement des shaders▲
Nous arrivons aux parties les plus intéressantes. Si nous utilisons Direct3D, nous voulons charger des programmes constitués de vertex shader et de pixel shader, si nous avons OpenGL, nous utiliserons des programmes constitués de fragments shaders ARB et de vertex shader. J'ai écrit les programmes correspondant dans les fichiers d3d8.ps, d3d8.vs, d3d9.ps, d3d9.vs, opengl.ps et opengl.vs. Nous avons pour le moment uniquement besoins de ces fichiers. Ceci est fait dans le switch qui suit. Notez qu'il n'est pas nécessaire d'écrire les shaders dans des fichiers textes comme dans cet exemple. Vous pouvez même écrire les shaders directement comme des chaînes de caractères dans le fichier source cpp, et utiliser addShaderMaterial() au lieu de addShaderMaterialFromFiles().
io::
path vsFileName; // Nom de fichier pour le vertex shader.
io::
path psFileName; // Nom de fichier pour le pixel shader.
switch
(driverType)
{
case
video::
EDT_DIRECT3D8:
psFileName =
"../../media/d3d8.psh"
;
vsFileName =
"../../media/d3d8.vsh"
;
break
;
case
video::
EDT_DIRECT3D9:
if
(UseHighLevelShaders)
{
// Cg peut aussi gérer cette syntaxe.
psFileName =
"../../media/d3d9.hlsl"
;
vsFileName =
psFileName; // Les deux shaders sont dans le même fichier.
}
else
{
psFileName =
"../../media/d3d9.psh"
;
vsFileName =
"../../media/d3d9.vsh"
;
}
break
;
case
video::
EDT_OPENGL:
if
(UseHighLevelShaders)
{
if
(!
UseCgShaders)
{
psFileName =
"../../media/opengl.frag"
;
vsFileName =
"../../media/opengl.vert"
;
}
else
{
// Utilise la syntaxe HLSL pour Cg.
psFileName =
"../../media/d3d9.hlsl"
;
vsFileName =
psFileName; // Les deux shaders sont dans le même fichier.
}
}
else
{
psFileName =
"../../media/opengl.psh"
;
vsFileName =
"../../media/opengl.vsh"
;
}
break
;
}
De plus, nous vérifions si la machine et le moteur de rendu sélectionné sont capables d'exécuter les shaders que nous souhaitons. Sinon, nous mettons simplement la chaîne de caractères du nom de fichier à 0. Ceci n'est pas nécessaire, mais très utile dans cet exemple : par exemple, si la machine est capable d'exécuter les vertex shader mais pas les pixels shader, nous créons un nouveau matériel qui n'utiliserait que les vertex shader et pas les pixels shader. Autrement, si nous voulions dire au moteur de créer le matériel et que le moteur voit que la machine n'est pas capable de répondre entièrement à la requête, il ne créera aucun nouveau matériel. Donc dans cet exemple, vous verrez au moins un vertex shader en action sans pixel shader.
if
(!
driver->
queryFeature(video::
EVDF_PIXEL_SHADER_1_1) &&
!
driver->
queryFeature(video::
EVDF_ARB_FRAGMENT_PROGRAM_1))
{
device->
getLogger()->
log("WARNING: Pixel shaders disabled "
\
"because of missing driver/hardware support."
);
psFileName =
""
;
}
if
(!
driver->
queryFeature(video::
EVDF_VERTEX_SHADER_1_1) &&
!
driver->
queryFeature(video::
EVDF_ARB_VERTEX_PROGRAM_1))
{
device->
getLogger()->
log("WARNING: Vertex shaders disabled "
\
"because of missing driver/hardware support."
);
vsFileName =
""
;
}
IV. Création des matériels▲
Maintenant, créons les nouveaux matériels. Comme vous l'avez peut-être compris des exemples précédents, un type de matériel dans le moteur Irrlicht est créé en changeant simplement la valeur du MaterialType dans la structure Smaterial. Cette valeur est une simple valeur sur 32 bits comme video::EMT_SOLID. Donc nous avons juste besoin du moteur pour créer une nouvelle valeur pour nous, que nous pouvons mettre ici. Pour ce faire, nous obtenons un pointeur sur et IGPUProgrammingServices appelle addShaderMaterialFromFiles(), qui retourne une nouvelle valeur sur 32 bits. C'est tout.
Les paramètres de cette méthode sont les suivants : premièrement, les noms des fichiers contenant le code du vertex et du pixel shader. Si vous voulez utiliser addShaderMaterial() à la place, vous n'avez pas besoin de noms de fichiers, vous pouvez alors écrire le code des shaders directement sous forme de chaîne de caractères. Le paramètre suivant est un pointeur sur la classe IShaderConstantSetCallBack que nous avons écrite au début de ce programme. Si vous ne voulez pas mettre de constantes, mettez-le à 0. Le dernier paramètre dit au moteur quel matériel doit être utilisé comme matériel de base.
Pour le montrer, nous créons deux matériels avec un matériel de base différent, l'un avec EMT_SOLID et l'autre avec EMT_TRANSPARENT_ADD_COLOR.
// Crée les matériels.
video::
IGPUProgrammingServices*
gpu =
driver->
getGPUProgrammingServices();
s32 newMaterialType1 =
0
;
s32 newMaterialType2 =
0
;
if
(gpu)
{
MyShaderCallBack*
mc =
new
MyShaderCallBack();
// Crée-les shaders selon si l'utilisateur veut des shaders haut ou bas niveau :
if
(UseHighLevelShaders)
{
// Choisit le type de shader désiré. Par défaut, c'est le type de shader natif du pilote, pour Cg donnez la valeur spéciale EGSL_CG.
const
video::
E_GPU_SHADING_LANGUAGE shadingLanguage =
UseCgShaders ? video::
EGSL_CG:video::
EGSL_DEFAULT;
// Crée le matériel à partir du shader haut niveau (hlsl, glsl or cg).
newMaterialType1 =
gpu->
addHighLevelShaderMaterialFromFiles(
vsFileName, "vertexMain"
, video::
EVST_VS_1_1,
psFileName, "pixelMain"
, video::
EPST_PS_1_1,
mc, video::
EMT_SOLID, 0
, shadingLanguage);
newMaterialType2 =
gpu->
addHighLevelShaderMaterialFromFiles(
vsFileName, "vertexMain"
, video::
EVST_VS_1_1,
psFileName, "pixelMain"
, video::
EPST_PS_1_1,
mc, video::
EMT_TRANSPARENT_ADD_COLOR, 0
, shadingLanguage);
}
else
{
// Crée le matériel à partir du shader bas niveau(asm or arb_asm).
newMaterialType1 =
gpu->
addShaderMaterialFromFiles(vsFileName,
psFileName, mc, video::
EMT_SOLID);
newMaterialType2 =
gpu->
addShaderMaterialFromFiles(vsFileName,
psFileName, mc, video::
EMT_TRANSPARENT_ADD_COLOR);
}
mc->
drop();
}
Maintenant il est temps de tester les matériels. Nous créons un cube de test et lui affectons le matériel créé. De plus, nous ajoutons un nœud de scène texte au cube ainsi qu'un animateur de rotation pour le rendre plus intéressant et important.
// Crée le nœud de scène de test 1, avec le nouveau matériel créé de type 1.
scene::
ISceneNode*
node =
smgr->
addCubeSceneNode(50
);
node->
setPosition(core::
vector3df(0
,0
,0
));
node->
setMaterialTexture(0
, driver->
getTexture("../../media/wall.bmp"
));
node->
setMaterialFlag(video::
EMF_LIGHTING, false
);
node->
setMaterialType((video::
E_MATERIAL_TYPE)newMaterialType1);
smgr->
addTextSceneNode(gui->
getBuiltInFont(),
L"PS & VS & EMT_SOLID"
,
video::
SColor(255
,255
,255
,255
), node);
scene::
ISceneNodeAnimator*
anim =
smgr->
createRotationAnimator(
core::
vector3df(0
,0.3
f,0
));
node->
addAnimator(anim);
anim->
drop();
La même chose pour le second cube, mais avec le second matériel que nous avons créé.
// Crée le nœud de scène de test 2, avec le nouveau matériel créé de type 2.
node =
smgr->
addCubeSceneNode(50
);
node->
setPosition(core::
vector3df(0
,-
10
,50
));
node->
setMaterialTexture(0
, driver->
getTexture("../../media/wall.bmp"
));
node->
setMaterialFlag(video::
EMF_LIGHTING, false
);
node->
setMaterialFlag(video::
EMF_BLEND_OPERATION, true
);
node->
setMaterialType((video::
E_MATERIAL_TYPE)newMaterialType2);
smgr->
addTextSceneNode(gui->
getBuiltInFont(),
L"PS & VS & EMT_TRANSPARENT"
,
video::
SColor(255
,255
,255
,255
), node);
anim =
smgr->
createRotationAnimator(core::
vector3df(0
,0.3
f,0
));
node->
addAnimator(anim);
anim->
drop();
Puis nous ajoutons un troisième cube sans shader dessus pour être capable de comparer les cubes.
// Ajoute un nœud de scène sans shader.
node =
smgr->
addCubeSceneNode(50
);
node->
setPosition(core::
vector3df(0
,50
,25
));
node->
setMaterialTexture(0
, driver->
getTexture("../../media/wall.bmp"
));
node->
setMaterialFlag(video::
EMF_LIGHTING, false
);
smgr->
addTextSceneNode(gui->
getBuiltInFont(), L"NO SHADER"
,
video::
SColor(255
,255
,255
,255
), node);
V. Ajout de la skybox▲
Et en dernier, nous ajoutons une « skybox » et une caméra contrôlée par l'utilisateur à la scène. Pour la texture de la « skybox », nous désactivons la génération de « mipmap » parce que nous n'en avons pas besoin dessus.
// Ajoute une jolie « skybox ».
driver->
setTextureCreationFlag(video::
ETCF_CREATE_MIP_MAPS, false
);
smgr->
addSkyBoxSceneNode(
driver->
getTexture("../../media/irrlicht2_up.jpg"
),
driver->
getTexture("../../media/irrlicht2_dn.jpg"
),
driver->
getTexture("../../media/irrlicht2_lf.jpg"
),
driver->
getTexture("../../media/irrlicht2_rt.jpg"
),
driver->
getTexture("../../media/irrlicht2_ft.jpg"
),
driver->
getTexture("../../media/irrlicht2_bk.jpg"
));
driver->
setTextureCreationFlag(video::
ETCF_CREATE_MIP_MAPS, true
);
// Ajoute une caméra et cache le curseur de la souris.
scene::
ICameraSceneNode*
cam =
smgr->
addCameraSceneNodeFPS();
cam->
setPosition(core::
vector3df(-
100
,50
,100
));
cam->
setTarget(core::
vector3df(0
,0
,0
));
device->
getCursorControl()->
setVisible(false
);
Maintenant, dessinons tout. Voilà, c'est fini.
int
lastFPS =
-
1
;
while
(device->
run())
if
(device->
isWindowActive())
{
driver->
beginScene(true
, true
, video::
SColor(255
,0
,0
,0
));
smgr->
drawAll();
driver->
endScene();
int
fps =
driver->
getFPS();
if
(lastFPS !=
fps)
{
core::
stringw str =
L"Irrlicht Engine - Vertex and pixel shader example ["
;
str +=
driver->
getName();
str +=
"] FPS:"
;
str +=
fps;
device->
setWindowCaption(str.c_str());
lastFPS =
fps;
}
}
device->
drop();
return
0
;
}
Compilez et exécutez, et j'espère que vous vous amusez avec notre nouvel petit outil d'écriture de shaders.
VI. Conclusion▲
Vous pouvez désormais utiliser des shaders et créer de nouveaux types de matériaux avec le Irrlicht.
Dans le prochain tutoriel Éclairage par pixels, nous verrons comment créer des matériels plus compliqués : les surfaces éclairées par pixels. Nous verrons aussi comment utiliser les systèmes de particules de brouillard et déplacement.
VII. Remerciements▲
Merci à Nikolaus Gebhardt de nous permettre de traduire ce tutoriel.
Merci à LittleWhite pour sa relecture technique ainsi qu'à zoom61 pour sa relecture orthographique.