IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Parsing de fichiers RIB avec Boost.Spirit


précédentsommairesuivant

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:

 
Sélectionnez
#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:

 
Sélectionnez
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"):

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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,

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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) :

 
Sélectionnez
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 :

 
Sélectionnez
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 :

 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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).
 
Sélectionnez
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.

 
Sélectionnez
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 :

 
Sélectionnez
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()) :

 
Sélectionnez
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.


précédentsommairesuivant