IV-A. Syntaxe▲
IV-A-0. Petit tour d'horizon des parsers fournis par Spirit et utilisé ici▲
Spirit fournit un certain nombre de parsers nous permettant d'effectuer un certain nombres de tâches basiques et communes à beaucoup d'analyses. Voici les parsers qui sont utilisés dans ce tutoriel :
- blank_p : reconnaît des espaces ou tabulations,
- eol_p : reconnaît une fin de ligne (au format Windows ou *nix, peu importe),
- comment_p : permet de définir un commentaire. Il vient sous plusieurs formes : en prenant un seul argument, il reconnaît un commentaire s'étendant jusqu'à la fin de la ligne. Si deux arguments lui sont passés, il s'agit alors d'un commentaire multi-lignes. Il y a aussi possibilité de gérer des commentaires imbriqués,
- str_p : reconnaît une chaîne de caractères littérale (c'est-à-dire que l'on veut lire précisément la chaîne passée en paramètre),
- alnum_p : reconnaît les caractères alpha-numériques,
- real_p : reconnaît un nombre réel au format double ; il s'agit d'un typedef sur un parser générique gérant les réels. Si vous voulez analyser des float uniquement, il vous suffit de faire un typedef similaire sur real_parser<float> par exemple,
- int_p : reconnaît un nombre entier en base 10 ; ici encore, il s'agit d'un typedef sur un parser de nombre plus générique, à savoir int_parser,
- repeat_p : il s'agit d'un analyseur particulier, car il ne reconnaît rien, mais permet de décrire une boucle. Nous y reviendrons plus tard.
IV-A-1. Squelette de la grammaire et commentaires▲
Avec Spirit, une grammaire est représentée par une classe dérivant de grammar<T> (où le paramètre template est la classe que
nous définissons, ce qui correspond à une utilisation de l'idiome
CRTP
). Le contrat de cette classe, du point de vue de Spirit, est de fournir la définition d'une autre classe template toujours nommée
definition. L'argument template de cette classe est le type de scanner utilisé (chose dont vous avez peu à vous soucier lorsque vous
débutez avec cette bibliothèque).
Cette classe definition doit fournir 2 méthodes:
- un constructeur prenant une référence constante vers la classe de grammaire,
- la méthode start() fournissant la règle de plus haut niveau afin de commencer l'analyse
Voici donc le squelette que nous allons enrichir au fur et à mesure des étapes:
#include
<boost/spirit.hpp>
#include
<boost/spirit/phoenix.hpp>
#include
"../scene/scene.h"
namespace
rib
{
using
namespace
boost::
spirit;
using
namespace
phoenix;
typedef
char
char_t;
typedef
file_iterator<
char_t>
iterator_t;
typedef
scanner<
iterator_t>
scanner_t;
typedef
rule<
scanner_t>
rule_t;
struct
Syntax : public
grammar<
Syntax>
{
Syntax(scene::
Scene &
target)
:
scn(target)
{
}
template
<
typename
ScannerT>
struct
definition
{
definition(Syntax const
&
self)
{
}
rule<
ScannerT>
const
&
start() const
{
return
root; }
// Définitions des règles
rule<
ScannerT>
root;
}
;
private
:
scene::
Scene &
scn;
}
;
}
Toutes les règles seront ajoutées dans le constructeur de definition, et toute règle sera fille à un quelconque niveau de root.
Une référence vers une Scene est fournie dans le constructeur de Syntax ; c'est sur cette scène que nous allons agir selon le contenu
du fichier.
Si vous lancer l'analyse avec le fichier tutoriel.rib fourni plus haut, et uniquement avec ce code, l'analyse n'ira pas bien loin. Elle échoue sur la première ligne avec le message:
Erreur d'analyse (offset 0
) a la ligne: "# tutoriel.rib"
En guise d'introduction aux règles, nous allons voir comment passer cette erreur, c'est-à-dire écrire le parser skip utilisé lors de
l'appel à la fonction parse (car comme dit plus haut, nous analysons par phrase). Nous allons donc ignorer 3 éléments: les commentaires,
les espacements et les fins de ligne.
Rapellons nous ce qui a été spécifié plus tôt pour indiquer la syntaxe d'un commentaire: un # hors d'une chaîne dénote le début d'un commentaire
(donc tout ce qui suit ce # peut être ignoré, sauf la fin de ligne). Spirit nous fournit directement un parser de commentaires, le parser
nommé comment_p. Nous l'utiliserons dans sa forme acceptant un argument qui est le caractère initial du commentaire, ici #.
Il ne nous reste plus maintenant qu'à intégrer les commentaires au parser skip avec l'opérateur | ("ou"):
parse_info<
iterator_t>
pInfo =
parse(fileBegin, fileEnd, syntax, blank_p |
comment_p('#'
) |
eol_p);
Avec ce parser skip, nous sommes maintenant capables d'ignorer les lignes vides, les lignes ne contenant que des espacements ou encore
les lignes ne contenant qu'un commentaire.
Cependant, le fait d'ignorer certaines parties du texte à analyser empêchera le rapport d'erreur d'être utile. Il se peut qu'il y ait des
erreurs sur une portion du texte suivant une ligne blanche ou un commentaire. Le parser s'arrêtera alors sur le commentaire ou la ligne vide.
Par conséquent, il est nécessaire afin de "manger" tout ce qui est ignoré par l'analyseur. Nous réalisons ceci avec un second appel à
parse, dont la règle d'analyse est la règle déterminant les éléments ignorés.
L'ensemble du code nous permettant d'analyser le fichier et faire un rapport d'erreur basique devient donc :
bool
Loader::
load(const
std::
string &
ribFilename, Scene &
target)
{
iterator_t fileBegin(ribFilename);
if
(!
fileBegin)
{
cout <<
"Impossible d'ouvrir "
<<
ribFilename <<
"."
<<
endl;
return
1
;
}
iterator_t fileEnd(fileBegin.make_end());
Syntax syntax(target);
rule<
scanner_t>
skipParser =
blank_p |
comment_p('#'
) |
eol_p;
parse_info<
iterator_t>
pInfo =
parse(fileBegin, fileEnd, syntax, skipParser);
pInfo =
parse(pInfo.stop, fileEnd, skipParser);
if
(!
pInfo.full)
{
if
(pInfo.hit)
cout <<
"Erreur d'analyse (offset "
<<
pInfo.length <<
") a la ligne:
\"
"
;
else
cout <<
"Erreur d'analyse (offset 0) a la ligne:
\"
"
;
// Ecris l'erreur sur la sortie standard
char_t currentChar =
*
pInfo.stop;
bool
isNewLine =
currentChar ==
'
\n
'
||
currentChar ==
'
\r
'
;
while
(currentChar !=
0
&&
!
isNewLine)
{
cout <<
currentChar;
++
pInfo.stop;
currentChar =
*
pInfo.stop;
isNewLine =
currentChar ==
'
\n
'
||
currentChar ==
'
\r
'
;
}
cout <<
"
\"
"
<<
endl;
}
return
pInfo.full;
}
Si nous lançons l'analyse de notre fichier test, nous pouvons constater que ça fonctionne et que l'erreur est maintenant un peu plus loin, là où commence réellement le travail d'analyse :
Erreur d'analyse (offset 46
) a la ligne: "Display "
exemple.tif" "
file" "
rgb""
IV-A-2. Règles générales▲
Nous allons dans un premier temps définir des règles générales, c'est à dire des règles qui peuvent être utilisées au cours de
n'importe quelle requête.
Commençons donc par définir ce qu'est une chaîne de caractères dans un fichier RIB. Nous prendrons dans le cas présent une définition simplifiée, exempte des caractères échappables (détails disponible dans l'appendice C de la spécification, section 2.1). Nous considérons comme une chaîne de caractères tout les nombres, toutes les lettres et le point comme symbole valide, entourés de guillemets. Notre règle finalement écrite est donc :
ribString =
'"'
>>
(+
(alnum_p |
'.'
)) >>
'"'
;
(Notez le parenthèsage des régles alternatives : c'est très important de bien grouper les analyseurs.)
Un autre type couramment utilisé dans un fichier RIB est le vecteur, qui comme la grande majorité des vecteurs en 3D (hors utilisation de
coordonnées homogènes) est composée de trois valeurs : x, y et z (qui sont des réels).
Un vecteur peut être écrit sous deux formes comme il a été précisé plus haut : 3 réels ou un tableau de 3 réels. Pour éviter de nous
répéter les trois composantes, nous pouvons abstraire cette partie dans une règle appelée ribInnerVector que nous
réutilisons dans la régle ribVector qui indique le choix entre les deux notations :
ribInnerVector =
real_p >>
real_p >>
real_p;
ribVector =
ribInnerVector |
('['
>>
ribInnerVector >>
']'
);
Pour les couleurs, la même approche s'impose. Il y a toutefois une différence importante entre un vecteur et une couleur ; là où le vecteur
est restreint à trois composantes, la couleur peut en avoir n. En effet, la requête ColorSamples permet
de spécifier différents espaces couleurs et donc les composantes utilisées.
Deux possibilités s'offrent alors à nous : lire autant de réels qu'il y en a de disponibles, ou boucler sur le nombre de composantes définies
dans la scène (trois par défaut, sinon c'est défini par le pré-nommé ColorSamples). La première approche est la plus simple à mettre en
oeuvre, malheureusement elle permet aussi de lire un nombre incorrect de composantes ce qui nous obligera à faire cette vérification plus
tard.
Boucler semble tout à fait adapté, mais comment faire ? Spirit fournit un analyseur nommé repeat_p. Il permet de répéter
une règle (spécifiée entre crochets) un nombre de fois arbitraire. Par exemple,
repeat_p(3
) [ real_p ]
lira 3 réels précisément. Il reste un dernier écueil : comment demander à boucler un nombre de fois qui va varier selon ce que l'on a analysé précédemment ? Boost.ref va nous permettre de résoudre ce problème. Nous allons donc avoir une méthode dans la scène qui renvoie un boost::reference_wrapper<int const> qui décore une référence constante vers le nombre de composantes dans une couleur :
class
Scene
{
public
:
Scene() : m_ColorSize(3
) {}
boost::
reference_wrapper<
int
const
>
getColorSize() const
{
return
boost::
cref(m_ColorSize); }
private
:
int
m_ColorSize;
}
;
Il ne nous reste plus qu'à passer le résultat de cette méthode lors de la construction du parser repeat_p, et nous
pouvons désormais lire un nombre de composantes arbitraires tout en le modifiant à la volée si le fichier rib l'impose.
ribInnerColor =
repeat_p(self.scn.getColorSize()) [ real_p ];
ribColor =
ribInnerColor |
( '['
>>
ribInnerColor >>
']'
);
Le format RIB utilise aussi souvent les tableaux. Ils peuvent servir de paramètres pour diverses requêtes, mais aussi comme
paramètres passés aux shaders. Un tableau est décrit comme un conteneur homogène (c'est-à-dire qu'il n'y a qu'un seul type de données
contenu) entouré par un crochet ouvrant et un crochet fermant. Les types pouvant être contenus sont tout les types supportés par le
format RIB: chaîne, réels, couleurs etc...
Il est donc tentant d'écrire cette règle ainsi :
ribArray =
'['
>>
(+
real_p |
+
ribString |
+
ribInnerVector |
+
ribInnerColor) >>
']'
;
Il y a toutefois un problème avec cette écriture : les règles décrites par ribInnerVector et ribInnerColor consommeront toutes deux des réels. Pour peu que le nombre de composantes d'une couleur soit égale à celles des vecteurs, il est devient impossible de les différencier. L'approche choisie ici délèguera donc l'interprétation du contenu du tableau au destinataire et ne stockera que les primitives que sont les chaînes de caractères ou les réels (lesquelles sont préfixées par un "+" afin d'indiquer qu'il y a au moins un élément) :
ribArray =
'['
>>
(+
real_p |
+
ribString) >>
']'
;
Dernier type à aborder pour ce paragraphe : les paramètres que nous passerons aux shaders. Un paramètre est toujours nommé, et le type qui lui est passé peut être n'importe quel type supporté par le format RIB. Par exemple :
Surface
"plastic"
"Kd"
0
.75
fait en sorte que l'on passe la valeur 0.75 au paramètre "Kd" du shader "plastic". Sachant ceci, nous écrivons cette règle ainsi :
ribParameter =
ribString >>
(real_p |
ribVector |
ribArray |
ribColor |
ribString);
IV-A-3. Définition des requêtes▲
Ce paragraphe contient les règles décrivant les requêtes appartenant aux options du fichier RIB ; il s'agit de la section 4.1 de la spécification RenderMan :
- Les requêtes Format et Projection appartiennent aux options de caméra (§4.1.1),
- Display à celles de l'affichage (§4.1.2),
- et ColorSamples aux options additionnelles (§4.1.3),
Une requête Projection, si l'on se réfère au standard, est décrite par le mot clé "Projection", suivi d'une chaîne de caractère décrivant
le type de projection choisi et d'un éventuel paramètre. Nous n'utilisons pas ici le type ribString défini plus haut, car il est dans notre
intérêt de ne permettre que les types de projections légales afin de refuser, si nécessaire, le fichier directement à l'analyse.
L'opérateur '!' permet de spécifier qu'une règle doit être vérifiée au plus une seule fois. Remarquez par ailleurs l'utilisation du parser
str_p afin de forcer le type de chaque partie de l'expression. Sans, le compilateur va penser que l'on essaie d'utiliser
l'opérateur de décalage de bits avec une chaîne de caractère et un caractère, ou encore tenter d'unir deux chaînes de caractères.
projection =
str_p("Projection"
) >>
'"'
>>
(str_p("perspective"
) |
"orthographic"
) >>
'"'
>>
!
ribParameter;
La requête Format permet tout simplement de définir la résolution de l'image à rendre. Les paramètres attendus sont la résolution horizontale et verticale, représentées par deux entiers, ainsi qu'un réel définissant l'aspect ratio d'un pixel :
format =
"Format"
>>
int_p >>
int_p >>
real_p;
La commande Display permet d'indiquer sous quel format et où stocker le résultat du rendu. Le standard nous indique que les paramètres sont
- une chaîne nommant la sortie (ce peut être le nom du fichier ou le titre de la fenêtre, selon la cible),
- une chaîne indiquant la cible ; toute implémentation se doit de supporter le type "file" (pour stocker l'image un fichier) ainsi que "framebuffer" (qui va afficher l'image dans une fenêtre sans la sauvegarder). Ici encore, il est choisi de les expliciter,
- une chaîne représentant le mode de rendu,
- ainsi que d'éventuel(s) paramètre(s).
display =
"Display"
>>
ribString >>
'"'
>>
(str_p("framebuffer"
) |
"file"
) >>
'"'
>>
ribString >>
*
ribParameter;
ColorSamples est la dernière requête exposée dans cette partie ; comme expliqué plus haut, c'est par elle qu'il est possible de modifier
le nombre de composantes formant une couleur. Ses paramètres sont au nombre de deux seulement. Le premier (appelé nRGB) est un
tableau qui permet de convertir une couleur représentée par n composantes vers du RGB classique ; il s'agit donc d'une matrice de
n lignes et trois colonnes. Le second (appelé RGBn), sert à convertir des couleurs exprimées RGB vers des couleurs utilisant
n canaux ; il est donc d'une taille de trois lignes sur n colonnes. Puisque les matrices ne sont pas directement supportées
par le format RIB, il s'agit dans les deux cas d'un tableau de n*3 réels.
Le nombre de composantes dans une couleur sera donc dérivé, plus tard, du nombre d'éléments lu dans ces tableaux.
colorSamples =
"ColorSamples"
>>
ribArray >>
ribArray;
Afin de faciliter la lecture de la syntaxe, je regroupe les règles en catégorie représentant les paragraphes de la spécification RenderMan. Il s'agit simplement d'assembler les règles avec l'opérateur OU :
cameraRules =
projection |
format;
displayRules =
display;
addOptionsRules =
colorSamples;
optionRules =
cameraRules |
displayRules |
addOptionsRules;
Il ne serait guère utile de présenter toutes les règles verbatim puisqu'aucune nouvelle technique de Spirit n'y est utilisée. Leur
implémentation est donc laissée comme excercice au lecteur intéressé.
Après avoir codé les règles correspondant à chacune des requêtes, il ne reste plus qu'à faire en sorte que l'analyse se fasse ; c'est
à dire en créant la règle root (puisque c'est celle qui est renvoyée par la fonction
definition::start()) :
request =
graphicStateRules |
geomPrimRules;
root =
*
request;
Nous voici maintenant capable de consommer l'intégralité du fichier RIB présenté en I-B
Vous trouverez à cette adresse une archive téléchargeable contenant l'intégralité du code nécessaire à l'analyse de ce fichier RIB.