Parsing de fichiers RIB avec Boost.Spirit

Denys Bulant

Un fichier RIB (RenderMan Interface Bytestream) est un descripteur de scène 3D; il peut être présenté sous la forme d'un fichier texte ou binaire. Ce tutoriel va expliquer les bases du parsing d'un fichier RIB ASCII grâce à la bibliothèque Boost.Spirit.

- Titre : Parsing de fichiers RIB avec Boost.Spirit

- Auteur : Denys Bulant

- Parution : 29 mars 2009

- Licence :

Sommaire

- I. Introduction

- I-A. Qu'est ce que Boost.Spirit ?

- I-B. Qu'est ce qu'un fichier RIB ?

- I-C. L'objectif de ce tutoriel

- I-D. Existant assumé

- II. Pour commencer...

- II-A. Analyse d'une std::string

- II-B. Analyse d'un fichier

- II-C. Indication des erreurs: 101

- III. Etude du fichier à analyser

- IV. Analyse

- IV-A. Syntaxe

- IV-A-0. Petit tour d'horizon des parsers fournis par Spirit et utilisé ici

- IV-A-1. Squelette de la grammaire et commentaires

- IV-A-2. Règles générales

- IV-A-3. Définition des requêtes

- IV-B. Actions sémantiques

- IV-B-1. Avant d'entamer...

- IV-B-2. Règles générales

- IV-B-3. Options

- IV-B-4. Attributs

- IV-B-5. Transformations

- IV-B-6. Primitives géométriques

- V. Conclusion

- VI. Copyright

I - Introduction

Avant de commencer, je me dois de préciser que je ne suis en aucun cas un expert en langages et que je n'ai que peu d'expérience sur ce sujet. Si d'aventure vous veniez à trouver une information fausse, tout retour sera le bienvenu afin d'améliorer le contenu de ce tutoriel.

I-A - Qu'est ce que Boost.Spirit ?

Dans la petite famille des parsers C++, Boost nous fournit une bibliothèque de grande qualité, nommé Spirit.
Spirit est un analyseur syntaxique LL, c'est à dire qu'il va parser l'entrée de gauche à droite et parser récursivement par la gauche (en anglais LL signifie Left to right -pour le parcours- et Leftmost derivation -pour préciser où se fait l'analyse récursive).
La syntaxe choisie par l'auteur se veut proche de la syntaxe EBNF (Extended Bachus-Normal Form); cette syntaxe est possible grâce à un usage poussé des templates C++.
Voici, tiré de la page d'introduction de cette bibliothèque, une syntaxe EBNF suivi de sa correspondance avec Spirit:

group       ::= '(' expression ')'
factor      ::= integer | group
term        ::= factor (('*' factor) | ('/' factor))*
expression  ::= term (('+' term) | ('-' term))*

group       = '(' >> expression >> ')';
factor      = integer | group;
term        = factor >> *(('*' >> factor) | ('/' >> factor));
expression  = term >> *(('+' >> term) | ('-' >> term));

Quelques concessions ont due être faites afin de coller à la syntaxe du C++ : les espaces signifiant un enchainement de règles sont remplacés par l'opérateur d'extraction de flux ('>>'), ou encore l'étoile servant à indiquer la présence de 0 ou plusieurs éléments répondant à une règle précéde la règle au lieu de la suivre.
En sus de cette syntaxe, Spirit permet de définir des actions sémantiques a exécuter lors du succès de l'analyse d'une règle:

std::vector<double> v;

numbers      = *(real_p[push_back_a(v)] >> *space_p);

Dans cet exemple, le vector v sera rempli de tout les chiffres présent dans l'entrée et séparés par un ou plusieurs espaces. Si l'entrée est "1 2 3.14 42 5", alors le contenu du vector v sera

v[0] == 1,
v[1] == 2,
v[2] == 3.14,
v[3] == 42,
v[4] == 5

Les règles real_p (reconnaissance d'un réel) et space_p (tout caractère d'espacement), ainsi que l'action push_back_a (empilement de la valeur reconnue dans un conteneur exposant la méthode push_back()) sont fournies par Spirit. Nous reviendrons plus tard à tout ceci, ne paniquez pas si vous ne comprenez pas encore !

I-B - Qu'est ce qu'un fichier RIB ?

Pixar a publié en 1988 des spécifications permettant de standardiser un format de fichier entre applications de modélisations et systèmes de rendu désirant être compatible RenderMan. Cette spécification, nommée RenderMan Interface Specification, a depuis vu quelques mises à jours. La version actuelle est la 3.2.1 (datant de Novembre 2005) et est disponible sous la forme d'un PDF à cette adresse (la version 3.1 est consultable en ligne).

Ce document contient principalement 2 parties:

1. Interface RenderMan

2. Langage de Shading RenderMan

Nous allons nous intéresser à la première partie de ce document, l'Interface RenderMan. Cette interface formalise la description d'une scène 3D sous la forme d'un fichier texte (ASCII) ou binaire. La première forme sera celle que nous allons parser avec Boost.Spirit.
Cette description peut prendre la forme suivante:

# tutoriel.rib
# Exemple de fichier RIB

Display "exemple.tif" "file" "rgb"
Projection "perspective" "fov" 40
Format 320 240 1

ColorSamples [1 0 0 0 1 0 0 0 1] [1 0 0 0 1 0 0 0 1]

LightSource "distantlight" 1 "intensity" 1
"from" [1 0 0] "to" [0 0 1]

LightSource "ambientlight" 2 "lightcolor" [0.3 0.3 0.3]

# définition du placement de la caméra
Translate 0 0 5
Rotate 45 0 0 1
Rotate 5 0 1 0

WorldBegin
# définition d'un (pseudo-) pacman :)
Color 1 1 0
Surface "plastic" "Kd" 0.75
Sides 2
Sphere 1 -1 1 280

# Une grosse sphère grise entourant la précédente
Color [.5 .5 .5]
Surface "matte"
Sphere 10 -10 10 360
WorldEnd

Dans le cas où vous voudriez vous initier à l'écriture de scènes sous ce format, il existe diverses implémentations compatibles RenderMan dont Aqsis ou encore Pixie (tout 2 des implémentations gratuites et open source).
L'image produite (Aqsis) par ce fichier est la suivante :

I-C - L'objectif de ce tutoriel

Ce tutoriel a pour but de parser le fichier RIB présenté ci-dessus grâce à Boost.Spirit. Le parser étudié au long de ce tuto sera capable de reconnaître les commandes utilisées dans ce fichier, non seulement pour les valeurs données ci-dessus, mais pour toutes les valeurs possibles pour les commandes utilisées.

Pour suivre ce tuto, je vous recommande de vous munir de la spécification RenderMan 3.2.1 ainsi que de garder la documentation de Spirit à portée de main.
L'affichage du résultat de ce fichier ne sera pas étudié dans ce tutoriel.

I-D - Existant assumé

Lors de l'analyse de tels fichiers, il y a toute une infrastructure à avoir: gestion de la scène bien sûr, mais aussi des couleurs ou encore des transformations. Ces classes n'étant pas l'objet de ce tutoriel, elles n'y seront pas étudiées (sauf dans le cas où une connaissance approfondie d'une méthode est nécessaire à la compréhension de l'analyse syntaxique). L'archive fournissant le code final du tutoriel, disponible en fin de ce tutoriel, les contiendra mais rien n'est fourni pour l'affichage.

Voici une courte présentation des modules utilisés:

- math: contient les classes mathématiques nécessaires aux positions, aux informations de directions etc...

- rib: permet de charger un fichier rib, c'est le module qui nous intéresse ici; voici donc quelques infos supplémentaires

- Loader: cette classe permet de charger un fichier et lancer le parsing

- Syntax: cette structure définit la syntaxe d'un fichier rib, ainsi que les actions à entreprendre quand une régle correspond

II - Pour commencer...

Spirit offre la possibilité d'analyser une entrée (chaîne de caractère ou fichier texte principalement grâce à une fonction libre nommée parse (d'autres méthodes existent et vous pouvez aussi écrire vos propres parsers). Cette fonction permet de parcourir l'entrée selon deux méthodes:

1. Analyse par caractère : chaque symbole est analysé consécutivement, il n'y a pas de notion de groupe de symboles

2. Analyse par phrase : l'entrée est analysée par groupes de symboles (suite de "mots" formant des "phrases") ; ce type d'analyse permet de spécifier une règle à ignorer lors de l'analyse de l'entrée

Lors d'une analyse par phrase, il faut spécifier à l'analyseur quelle règle ignorer: ce peut être des espaces si ce caractère n'est pas important (à l'image du C, du C++ etc...) et éventuellement des commentaires.

Les informations contenues dans les 3 points qui vont suivre sont cruciaux à la compréhension du reste du tutoriel. Si quelque chose vous paraît encore un peu flou à la fin de ce chapitre, je vous conseille d'y revenir et de prendre le temps de le comprendre. Cela facilitera d'autant votre compréhension du reste de ce tutoriel!

II-A - Analyse d'une std::string

Nous allons voir rapidement comment lancer l'analyse d'une chaîne de caractères (nous reprendrons l'exemple donné en ):

#include <algorithm>
#include <iostream>

#include <boost/spirit.hpp>

using namespace boost::spirit;
using namespace std;

int main(int argc, char **argv)
{
const string input("1 2 3.14 42 5");
vector<double> values;
parse_info<string::const_iterator> pInfo = parse(input.begin(), input.end(),
*(real_p[push_back_a(values)] >> *space_p)
);

copy(values.begin(), values.end(), ostream_iterator<double>(cout, "\n"));

return 0;
}

Sur la ligne de commande, nous obtenons bien les 5 doubles contenus dans notre string:

1
2
3.14
42
5

Analysons d'un peu plus près la ligne qui s'est occupé d'analyser la chaîne:

parse_info<string::const_iterator> pInfo = parse(input.begin(), input.end(),
*(real_p[push_back_a(values)] >> *space_p)
);

Nous passons ici à la fonction template boost::spirit::parse(...) 3 arguments:

1. IteratorT const& first : itérateur vers le premier caractère à analyser,

2. IteratorT const& last : itérateur vers le dernier caractère à analyser + 1,

3. parser<DerivedT> const& p : la règle servant à l'analyse.

Notons ici que cette fonction dispose d'une surcharge acceptant un paramètre optionnel qui est le parser skip. Lorsque la fonction est utilisée sans ce parser, l'analyse se fait au niveau du caractère (plus petit élément analysable). S'il est fourni, alors l'analyse est faite au niveau de la phrase. Le choix est fait ici de travailler par caractère car tout les caractères sont essentiels. En effet, si nous décidions de sauter les espaces et de procéder à une analyse par phrase, nous aurions obtenu la sortie 123.14425, car les espaces auraient été ignorés lors de l'analyse.
Par contre, si la syntaxe à analyser dispose de séparateurs clairs, alors l'analyse et l'écriture de règles sont plus simples au niveau de la phrase. Le C++, le C... sont tous des langages pouvant être analysés à ce niveau car les espaces et les commentaires ne sont pas utiles à l'analyse et les ignorer ne brise pas la syntaxe.

Après ce petit détour, revenons à notre analyse, et plus précisément au type de retour de parse(). La structure template boost::spirit::parse_info<IteratorT> doit avoir pour paramètre template le même type que les itérateurs spécifiés pour parse(). Cette structure nous donne quelques informations sur l'état de l'analyseur après l'analyse ; en voici sa composition:

- bool full : true si l'analyse a consommée tout les éléments fournis, false dans le cas contraire,

- bool hit : true si l'analyse a pu consommé au moins une partie de l'entrée, false dans le cas contraire,

- IteratorT stop : itérateur pointant vers l'élément où le parser s'est arrêté (input.end() si full est true),

- size_t length : indique le nombre de caractère(s) ayant été consommé par l'analyseur.

Il est donc aisé de fournir un retour minimal sur l'erreur (offset depuis le début de l'entrée, élément ayant causé problème) sans entrer dans d'autres détails (un code illustrant ceci sera donné plus bas).

Voyons maintenant le coeur de l'analyse, la règle :

*(real_p[push_back_a(values)] >> *space_p)

Qu'est-ce donc que cet enchaînement cryptique ? Cette règle se décompose en plusieurs éléments:

- real_p : cette règle permet de reconnaître des nombres réels (des doubles plus précisément),

- space_p : il s'agit d'une règle qui saura reconnaître tout type d'espacements (espaces, tabulations, retours chariot et nouvelles lignes)

- push_back_a : il s'agit d'une action sémantique qui va ajouter l'élément au conteneur passé en paramètre (le conteneur doit disposer de la méthode push_back(), et les objets contenus doivent être du type de ce que la règle va renvoyer)

Spirit suit une convention de nommage qui permet d'identifier facilement plusieurs éléments :

- ce qui finit par _p (real_p, space_p...) correspond aux analyseurs fournit avec Spirit,

- ce qui finit par _a (push_back_a, assign_a...) correspond à des actions sémantiques

Le principe des actions sémantiques est d'exécuter du code lorsque la règle à laquelle l'action est rattachée est analysée avec succès. Ce rattachement est signifié en l'écrivant entre crochets, après la règle qui lui est liée. Les actions sémantiques sont en quelque sorte des wrappers vers du code à exécuter car ils ne sont pas utilisés immédiatements. Pour cette raison, il ne faut pas utiliser une fonction normale directement: real_p[values.push_back] ne compilera pas!

Nous savons que nos chiffres sont séparés d'au moins un espace, sauf le dernier élément. Cet enchaînement est signifié par l'utilisation de l'opérateur >> qui indique qu'une règle, si elle est matchée, est suivi d'une autre régle. La quantité d'espacements étant variables, nous utilisons ici l'opérateur * qui indique que la règle qui suit immédiatement peut être répétée 0 ou n fois.
Dernier point, nous ne savons pas combien de couple "réel/espacement(s)" nous avons dans la chaîne. Il est donc nécessaire de la répéter 0 ou n fois elle aussi, mais comment faire ? Une règle peut être créée par une combinaison de plusieurs règles entourées de parenthèses. Par déduction, faire précéder notre règle nouvellement créée par l'opérateur * permet de formaliser cette répétition éventuelle.

II-B - Analyse d'un fichier

Analyser une chaîne, c'est bien pratique, mais comment analyser un fichier texte ?
La librairie Spirit fournit à cet effet un itérateur bien particulier, j'ai nommé file_iterator. Il différe des itérateurs fournis par la STL en ceci qu'il n'est pas seulement un input iterator, mais surtout un random access iterator. Les input iterator ne peuvent être utilisés avec Spirit dû à sa méthode d'analyse. Voici comment l'utiliser:

#include <boost/iterator/file_iterator.hpp>

typedef char                     char_t;
typedef file_iterator<char_t>    iterator_t;
(...)
string filepath("file.txt");
iterator_t fileStart(filepath);
if (!fileStart)
{
cout << "Impossible d'ouvrir " << filepath << " !" << endl;
return;
}

iterator_t fileEnd = fileStart.make_end();
parse_info<iterator_t> pInfo = parse(fileStart, fileEnd, *(real_p >> *space_p));

Un file_iterator doit être paramétré par le type de base qu'il est sensé lire à partir du fichier. Le constructeur prend 3 formes:

- file_iterator() : le constructeur par défaut construit un itérateur invalide,

- file_iterator(std::string filename) : ce constructeur accepte le nom du fichier à ouvrir,

- file_iterator(const base_t& iter) : ce constructeur accepte une référence vers le type de base défini (dans l'exemple, un char_t, c'est à dire, un char.

Si le fichier a pu être ouvert, alors l'itérateur est valide. La création de l'itérateur marquant la fin de l'entrée se fait en appelant la méthode file_iterator<>::make_end().
Il ne reste plus qu'à passer ces 2 itérateurs à la fonction parse, et le tour est joué!

II-C - Indication des erreurs: 101

Avant d'entrer dans le vif du sujet, il nous faut un moyen de savoir à quel endroit de la chaîne l'analyse échoue. Le debugger pourrait l'indiquer, mais l'obtension de la ligne ayant fait échouer l'analyse peut être un peu laborieux, sans compter que des erreurs de ce type, au cours de la rédaction d'une grammaire, on en rencontre souvent :)
Voici donc le snippet que j'utilise après un appel à parse pour indiquer le lieu de l'erreur:

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: \"";

while (*pInfo.stop != 0 && *pInfo.stop != '\n' && *pInfo.stop != '\r')
{
cout << *pInfo.stop;
++pInfo.stop;
}
cout << "\"" << endl;
}

III - Etude du fichier à analyser

Avant de commencer à coder, penchons-nous rapidement sur le format d'un fichier RIB. Comme spécifié plus haut, il s'agit d'un fichier texte ASCII. Chaque commande se présente selon cette syntaxe:
Requête param01 param02 ... paramn
Une requête peut avoir plusieurs formats (différents types d'arguments ou nombre d'arguments différent) si besoin est.

Les paramètres que nous allons supporter sont de type (il y a bien plus de types existants, mais nous nous limiterons à ceux-ci dans ce tutoriel) :

- Chaîne de caractères (ex: "exemple.tif"),

- Réél (ex: 3.14) ; si un entier est fourni, il sera automatiquement accepté comme nombre réél,

- Tableaux de chaînes de caractères ou de rééls (exclusivement l'un ou l'autre): un tableau est entouré de [ ] (ex: [0.3 0.3 0.3] est un tableau contenant 3 rééls valant chacun 0.3).

- Une couleur : elle peut être écrite sous la forme d'un tuple de n éléments ou sous la forme d'un tableau de n éléments (ex: "1 0 0" et "[1 0 0]" sont des couleurs identiques formées de 3 composantes)

- Un vecteur, point ou une normale : elle peut être écrite sous la forme d'un tuple de trois éléments (x, y et z) ou sous la forme d'un tableau de trois éléments (ex: "1 0 0" et "[1 0 0]" sont des vecteurs identiques)

Un dernier élément qu'il nous faudra analyser, ou plus exactement ignorer, sont les commentaires. Un commentaire commence par un # et se termine à la fin de la ligne (à l'exception des # contenus dans une chaîne où il est traité comme tout autre caractère) :

# Ceci est un commantaire
"# Ceci est une chaîne"

Attention cependant si vous désirez aller plus loin avec les fichiers RIB: une certaine catégorie de commandes sont passées au renderer sous une forme s'approchant des commentaires. Il s'agit des indications structurelles ; elles sont composées de ## suivi immédiatement d'un mot clé, par exemple :

##Frames 60

Dernier point, mais non des moindres, l'analyse sera faite ici par phrase. C'est à dire que nous ignorerons tout ce qui est espacement, retour à la ligne ou encore commentaires car ils ne sont pas important à la bonne analyse du fichier. En bonus, cela permet une écriture plus concise de notre grammaire, en nous permettant de nous concentrer uniquement sur ce que nous avons besoin d'analyser.

Passages concernés de la RISpec:
Définition des types: Section I.2.2 et Appendice C
Indications structurelles: Appendice D (§ D.1.3)
Structure générale recommandée d'un fichier RIB: Appendice D (§ D.1 en intégralité)

IV - Analyse

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.

IV-B - Actions sémantiques

IV-B-1 - Avant d'entamer...

actions fournies
intro à boost.phoenix.bind
helpers phoenix: construct_<T>
closure et définition de la closure utilisée pour le parsing RIB

struct RibClosure : boost::spirit::closure<RibClosure, std::string>
{
member1 str;
};

struct Syntax : public grammar<Syntax, RibClosure::context_t>
//...
typedef rule<ScannerT, RibClosure::context_t> rule_t;
// Rules...

IV-B-2 - Règles générales

String

ribString    = '"' >> (+(alnum_p | '.'))[ribString.str = construct_<std::string>(arg1, arg2)] >> '"';

vecteurs

// WIP...

IV-B-3 - Options

Display

display     = "Display" >> ribString[bind(&Scene::setTargetName)(var(self.scn), ribString.str)]
>> ribString
>> ribString;

Projection, Format

IV-B-4 - Attributs

LightSource, Color, Surface, Sides

IV-B-5 - Transformations

Translate, Rotate

IV-B-6 - Primitives géométriques

Sphere

V - Conclusion

VI - Copyright

The RenderMan® Interface Procedures and Protocol are:
Copyright 1988, 1989, 2000, 2005 Pixar
All Rights Reserved