Dernière mise à jour : 5/11/2000
Auteur : Artiste on the Web (A11W)
Des problèmes, des bugs, des corrections, maillez-moi !
Coupons court à tout baratin, et laissons parler la 3D :-)
Opale.Soya est un moteur de 3D Java construit par dessus OpenGL et GL4Java.
Entièrement orienté objet, la vitesse est incroyable et les effets spéciaux mortels :-]
Cette doc n'est absolument pas exhaustive car pour cela il faudrait un bouquin entier, mais donne un aperçu des fonctions principales.
opale.soya | Tout ce qui concerne l'initialisation. |
opale.soya.util | Tout ce qui n'est pas graphique, ou qui n'a pu trouver sa place ailleurs. Notemment les fonctions mathématiques (Matrix, Quaternion), les collections, et les objets se rapportant aux javaBeans. |
opale.soya.awt | Tous les composants AWT, dans lesquels Opale.Soya trace la 3D. Et les classes d'overlays. |
opale.soya.font | Toutes les polices de caractères. |
opale.soya.soya2d | Tout ce qui concerne la 2D. Notemment les textures, les materials,... |
opale.soya.soya3d | Tout ce qui concerne la 3D. Un package mamouth ! Notemment les éléments 3D de base, les interfaces de base comme Position, Orientation, Dimension, les ojets utilitaires comme Point ou Vector... |
opale.soya.soya3d.event | Tout ce qui concerne les événements de la 3D. MoveEvent,... |
opale.soya.soya3d.model | Tout ce qui concerne le modeling. Notemment les classes Shape, Fragment,... |
opale.soya.soya3d.geometry | Tout ce qui concerne les elements 3D géométriques : Sphere3D,... |
opale.soya.soya3d.fx | Tout ce qui concerne les effets spéciaux ! Sprite3D,... |
opale.soya.soya3d.fx.particles | Tout pour le système de particules ! ParticlesBunch3D,... |
opale.soya.soya3d.animation | Tout ce qui concerne les animations, dont Animation,... |
opale.soya.editor | Les classes génériques pour les éditeurs, notemment l'éditeur de beans. Les classes concernant l'édition d'un objet précis sont rangées dans le même package que cet objet. |
opale.soya.tutorial | Les tutoriels ! |
Animation (opale.soya.soya3d.animation.Animation) : Suite d'états (= state = "position" au sens large) pour un world donné (y compris tous les éléments qu'il contient). L'animation est créée à partir de plusieurs états, puis il est possible d'interpoler entre eux.
Animations (opale.soya.soya3d.animation.Animations) : Collection d'animation. Particulièrement utile pour regrouper et enregistrer ensemble plusieurs animation (pour un objet, un perso,...), car une animation ne décrit qu'un seul mouvement.
Camera (opale.soya.soya3d.Camera3D) : Un élément graphique 3D qui est utilisé comme renderer : comme interface entre un world (en 3D donc) et une rendering surface (composant AWT donc forcément en 2D).
Collection (opale.soya.util.Collection) : Un ensemble d'objet. Opale.Soya utilise sa propre classe de collections, qui étend celle de Java (java.util.Collection).
Elément (opale.soya.soya3d.Element3D) : Un élément en 3D, qui peut être ajouté dans un world : un monde en 3D (opale.soya.soya3d.World3D). Les éléments de bases sont les volumes, les worlds (mondes), les lights (lumières) et les cameras. Tous les noms de classes d'éléments 3D se terminent par 3D.
Elément graphique (opale.soya.soya3d.GraphicalElement3D) : Un élément 3D graphique. La plupart des éléments 3D le sont (sinon il ne sont plus vraiment 3D...).
Environnement (opale.soya.soya3d.Environment3D) : Un world, avec des propriétés d'environnement en plus, comme du brouillard, de l'éclairage ambiant...
Fragmented shape (opale.soya.soya3d.model.FragmentedShape) : Shape créé par additions de fragmented shape elements : triangles, quads... Un fragmented shape est une collection de fragmented shape elements.
Fragmented shape element (opale.soya.soya3d.model.FragmentedShapeElement) : Objets destinés à être insérés dans un fragmented shape, par exemple des triangles, des quadrangles (quad). Par exemple un cube est fabriqué à partir de 6 quads.
Material (opale.soya.soya2d.Material) : Un material est un objet qui regroupe toutes les propriétés de surface d'un objet en 3D : couleurs (au pluriel : couleur normal et couleur specular : brillante), brillance (shininess),... y compris une propriété texture. Un material peut etre partagé entre plusieurs objects (par exemple des shapes).
Rendering surface (opale.soya.awt.RenderingSurface) : Une surface de rendu pour Opale.Soya, correspondant souvent à un composant AWT (opale.soya.awt.RenderingCanvas).
Shape (opale.soya.soya3d.model.Shape) : Une forme (souvent géométrique) destiné à être "inséré" dans un volume. Par exemple un cube,... La quasi-totalité des shapes sont des fragmented shapes (voir ce terme). Un shape peut etre partagé entre plusieurs volumes.
Texture (opale.soya.soya2d.Texture) : Image bitmap 2D plaqué sur un polygone en 3D.
Volume (opale.soya.soya3d.Volume3D) : Un élément graphique 3D qui affiche un shape : une forme visible.
World (opale.soya.soya3d.World3D) : Un élément 3D (en fait un volume et donc un élément graphique) qui peut contenir d'autres éléments (donc aussi une collection).
Opale.Soya dessine sur une surface de rendu (opale.soya.awt.RenderingSurface), il s'agit généralement de composant AWT (opale.soya.awt.RenderingCanvas, et opale.soya.awt.RenderingFrame : une fenêtre AWT intégrant un canvas de rendu 3D : c'est ce composant qui est utilisé par les tutoriels).
opale.soya.awt.RenderingSurface se comporte exactement comme un canvas "normal"; la seule propriété importante est renderer. Il faut entrer dans cette propriété le renderer, généralement la caméra (monRenderingCanvas.setRenderer(maCamera)).
Overlay (opale.soya.awt.Overlay) : Un overlay est un graphique placé par dessus la 3D. Il peut s'agir de texte, d'image, voire de 3D...
Voir à ce sujet le tutoriel 1.
Opale.Soya représente ses mondes en 3D (worlds) de façon hérarchique, un peu à la manière d'un arbre.
A la base (le tronc ou la racine) se trouve un world, qui n'a pas de parent (il n'a été inséré dans aucun world); ce world est appelé le "root world".
Ce root world contient différents éléments (autant que nécéssaire, puisqu'un world est une collection d'éléments), y compris d'autres worlds, pouvant eux-aussi contenir d'autres worlds...
Si l'on veut avoir un rendu, il faut que, quelque part dans ce root world ou dans les worlds qui sont "dedans", il y aie au moins : une caméra (pour voir !) et quelque chose à voir (un volume par ex.).
Chaque élément dispose donc d'un world parent, qui correspond à sa propriété parent (méthode getParent()), à l'exeption du "root world", pour lequel la propriété parent vaut null. La méthode isInside(World3D) d'un élément permet de savoir si cet élément est dans le world (= si ce world est son parent ou si son parent est dans ce world).
Voir à ce sujet les tutoriels 1 et 5.
Nom : Chaque élément (opale.soya.soya3d.Element3D) possède un nom (propriété name). Ce nom peut être utiliser pour identifier ou retrouver l'élémént. Plusieurs éléments peuvent partager le même nom.
Diverses méthodes permettent de récupérer les éléments d'un world à partir d'un nom : element(name) retourne le premier élément ayant le nom demandé, elements(name) retourne un itérateur itérant tous les éléments de ce nom et search(name) est similaire mais accepte des jockers ("*", "?",...). De plus toutes ces méthodes ont une version récursive (elementRecursive, elementsRecursive, searchRecursive) qui effectue la recherche dans le world et tous les worlds qu'il contient.
Tous les éléments de base d'Opale.Soya, et la quasi-totatlité des autres, sont des éléments graphiques : par exemple les worlds, les volumes, les lumières, les caméras,... L'objet élément graphique correspond à un repère local, et implémente entre autres les 4 interfaces suivantes (du package opale.soya.soya3d):
CoordSyst (système de coordonnées) : Tout élément graphique définit un repère locale. Le vecteur x de ce repère correspond à la droite, le vecteur y à la direction du haut et le vecteur z à l'arrière, donc le vecteur -z est l'avant.
Position : L'élément a les propriétés x, y et z, correspondant à ses coordonnées cartésiennes. Ces coordonnées sont définis dans le repère parent (c'est à dire le world parent, celui dans lequel a été aljouté l'élément).
Les méthodes move(*) permettent de déplacer l'objet, la méthode addVector(vecteur) permet d'effectuer une translation.
Orientation : Lélément dispose de 3 vecteurs x, y et z.
Les méthodes rotate(*) permettent d'effectuer des rotations autour d'un axe donné. rotateLateral(angle) effectue une rotation autour de l'axe y, rotateVertical(angle) autour de l'axe x et rotateIncline(angle) autour de l'axe z. Les axes utilisés par ces trois méthodes dépendent de la propriété rotationType; si elle vaut ROTATION_TYPE_INTERNAL (valeur par défaut), les axes de l'élément sont utilisés; si elle vaut ROTATION_TYPE_EXTERNAL, les axes du repère parent sont utilisés.
Les méthodes lookAt(*) permmettent de diriger l'élément vers un point ou dans une direction donnée.
Dimension : L'élément a les propriétés width, height et depth, correspondant à ses dimensions selon les axes x, y et z (axe de l'élément).
Les méthodes scale(*) permettent d'agrandir ou de rétrécir l'élément.
Objets Point et Vector (opale.soya.soya3d.Point, opale.soya.soya3d.Vector) : Ces objets ne sont pas des éléments et ne peuvent donc pas être placés dans un world; ils sont juste utilisés pour effectuer des calculs sur des points ou des vecteurs. Par exemple la méthode addVector(Vector) permet de translater un point ou d'effectuer une addition vectorielle (selon qu'elle est effectuée sur un point ou un vecteur).
Opale.Soya définit un point ou un vecteur par ses 3 coordonnées cartésiennes (propriétés x, y, z) ET par une référence vers le repère local dans lequel ces coordonnées sont définies (propriété coordSyst). Notez que cette propriété est en lecture et écriture; la méthode setCoordSyst(CoordSyst) permet de changer de repère, et les coordonnées x, y, z sont converties dans le nouveau repère.
Si la méthode move(Position) d'une Position est appelée avec pour argument une Position définie dans un autre repère, une conversion de repère sera automatiquement effectuée, sauf si la propriété coordSyst est null (dans ce cas, aucune coversion n'est effectuée).
Exemple :
monVolume.move(new Point(0f, 0f, -1f, monVolume)); déplace monVolume de façon relative, en l'avançant d'une unité.
monVolume.move(new Point(0f, 0f, -1f, monVolume.getParent())); déplace monVolume de façon absolue, en le positionnant en (0, 0, 1) dans son repère parent.
Opale.Soya range la quasi-totalité des propriétés 2D dans des materials (opale.soya.soya2d.Material). Ces materials sont ensuite appliqués sur des formes géométriques en 3D.
Textures (opale.soya.soya2d.Texture et ses sous-classes) : Les textures sont considérées comme des propriétés des materials (contrairement à ce que font OpenGL ou d'autres moteurs de 3D). Il existe plusieurs classes de textures : TextureRGB pour celles qui ne sont pas transparentes, TextureRGBM pour celles qui sont transparentes avec un canal masque de type oui/non (pas de pixel semi-transparence ou translucide) et TextureRGBA pour celles avec un canal alpha (semi-transparence).
Les objets textures sont créées à partir de fichiers images (un pour la texture, plus un pour le masque dans le cas de TextureRGBM et TextureRGBA); les formats supportés sont ceux de java (gif, jpeg,...).
Couleurs : Chaque material dispose de deux couleurs : sa couleur normale (diffuseColor) et sa couleur de brillance (specularColor, normalement blanc).
Brillance (shininess): Représente le taux de brillance de la surface, entre 0 et 1. Pour un métal, la brillance sera proche de 0, et pour un plastique elle sera proche de 1.
Il existe encore beaucoup d'autres propriétés, mais leur usage est plutot rare, et vous devrez les expérimentez vous-même...
Dans Opale.Soya, la majorité des objets rendu à l'écran sont des volumes (à l'exception des effets spéciaux). Or tout ce qu'un volume sait faire, c'est de tracer un shape. Donc il vous faut à présent apprendre à créer et à manipuler ces shapes.
Shape est une classe abstraite générale; la classe concrète correspondante est fragmented shape (opale.soya.soya3d.model.FragmentedShape). Un fragmented shape est un shape créé par assemblage de morceaux : des fragmented shape elements (élément de fragmented shape).
Ces fragmented shape elements comprennent principalement les triangles et les quads (poly à 4 points), même si beaucoup d'autres peuvent être utilisés (sphere,...). Ainsi, 6 quads peuvent formé un cube. Chaque quads peut avoir des propriétés différentes : par exemple, la propriété material détermine tous les effets de surface (couleur, texture,...).
Cependant certaines formes complexes peuvent nécessiter un grand nombre de faces (= triangles ou quads). C'est pourquoi Opale.Soya convertit et assemble automatiquement ces fragmented shape elements en fragments, moins nombreux et plus rapidement tracés. La conversion est automatique, il n'y a rien à faire ni à s'en soucier !
Transformer un world en shape : Une autre possibilité, très utile pour créer de gros shapes, ou des shapes divisés en plusieurs parties similaires (par ex. un niveau de jeu avec des murs tous identiques,...) consiste à créer un world correspondant au shape désiré (avec des volumes et d'autres shapes), et à le transformer en shape avec la méthode toShape(). Le shape obtenu est un fragmented shape tout ce qu'il y a de plus normal.
Lumières statiques : Le nombre de lights (lumières) est limité d'une part par OpenGL (8 maxi en général) et aussi par le temps de calcul qu'elles impliquent. La technique ci-dessus permet aussi d'appliquer des éclairages statiques sur un shape (=des effets de lumière supplémentaires, indépendant des autres lumières, qui ne nécessitent presque pas de temps de calcul en plus et qui ne sont pas limités en nombre) : lorsqu'un world est transformé en shape, toutes les lumières qu'il contient et dont la propriété static est vrai sont appliquées de façon statique.
Voir le chapitre éclairage pour les autres effets de la propriété static.
Retournement des faces : les objets faces (opale.soya.soya3d.model.Face), comme les triangles et les quads, peuvent être visibles des deux côtés ou d'un seul. La seconde possibilité peut apporter un gain de vitesse non négligeable. Opale.Soya est capable de déterminer automatiquement l'intérieur ou l'extérieur d'une forme géométrique, et de n'afficher que l'un des deux (Par exemple n'afficher les faces d'un cube que depuis l'extérieur dans le cas d'un dé,...).
Voir la propriété visibility de l'objet face.
Optimisation : l'objet shape dispose d'une méthode optimize() qui, si elle peut prendre du temps, permet d'optimiser le shape. Cependant, le gain de performance est variable selon les OpenGL et les cartes 3D... pour moi il est nul :-(
Voir les tutoriels 2 pour le modeling.
Une fois que vous avez un shape, il suffit pour le tracer à l'écran de créer un volume, de mettre ce shape dans sa propriété shape (méthode setShape(shape)) et de l'ajouter dans un world (méthode add(element)). Le shape décrit juste la forme; le volume décrit la position, l'orientation et les dimensions. Il est donc possible d'utiliser le même shape dans plusieurs volumes.
Opale.Soya est capable de gérer 3 types d'éclairage :
Eclairage ambiant : Le plus facile à mettre en oeuvre. L'éclairage ambiant affecte tous les objets, indépendemment de leur position/orientation. Il est défini dans l'objet environnement, très similaire au world mais avec des propriétés d'environnement supplémentaires. La propriété ambiantColor (méthode setAmbiantColor(red, green, blue)) permet de le définir.
Voir le tutoriel 2.
Eclairage par des lumières (lights) : Le plus courant. L'élément graphique light est une source lumineuse. Il en existe trois type : les points (un point lumineux : une torche,...), les directionnels (une source lumineuse infiniment éloignée : le soleil,...) et les spots (un cone de lumière : un projecteur,...). Puisque les lights sont des éléments graphiques, il est possible de les déplacer, de les faire pivoter,...
Les propriétés des lights ne seront pas inconnus aux gouroux de l'OpenGL... :-)
La propriété color d'une light permet de régler sa couleur.
Les propriétés constantAttenuation, linearAttenuation et quadraticAttenuation d'une light permettent de régler son atténuation (= jusqu'où elle éclaire). La première est une atténuation indépendante de la distance, la seconde dépend de la distance et la troisième du carré de la distance. L'atténuation est sans effet sur les lights directionnels (puisque la distance est infinie).
Les propriétés spotAngle et spotExponent représente respectivement l'angle d'une lumière de type spot, et la différence de luminosité entre le centre et l'extérieur du spot.
Eclairage statique : Le plus complexe, le moins fréquent mais aussi le plus rapide... Voir le chapitre modeling pour la création de shapes éclairés par des lumières statiques.
L'éclairage statique permet un gain de vitesse dans le cas où un shape volumineux est éclairé par beaucoup de lumières, et si ces lumières sont immobiles par rapport au shape (par ex. une grande pièce éclairée par un luste).
Lorsque la propriété static d'une light est vrai, celle-ci est utilisée comme lumière statique lors de la création de shape; cependant elle n'éclaire plus les shapes qui ont un éclairage static (Puisque l'éclairage a déjà été réalisé de façon statique). Par contre, les autres shapes seront éclairés (dynamiquement).
La propriété pureStatic permet, si elle est vrai, de réaliser des lights purement statique : qui seront prises en compte lors de l'éclairage statique, mais qui n'éclaire aucun objet de façon dynamique. Ces lumières ont un rôle purement statique.
Ce chapitre explique la procédure de rendu utilisé par Opale.Soya. Sa lecture n'est donc absolument pas nécessaire pour utiliser Opale.Soya ! Par contre elle peut être intéressante si vous souhaitez étendre les classes d'Opale.Soya.
Le rendu commence par la collecte des Drawable (une interface pour les objets qui peuvent dessiner de la 3D). Cette collecte est réaliser par un DrawableCollector; c'est aussi à ce moment là que sont calculées toutes les matrices. Les drawables collectés sont triés au fur et à mesure par material, afin de minimiser les appels openGL lors du rendu.
Ensuite a lieu le rendu proprement dit, et les drawables sont tracés (méthode draw(...)), par ordre de material.
Le processus complet est évidemment beaucoup plus complexe...
Un environnement (opale.soya.soya3d.Environment3D) est un world disposant en plus de propriétés spéciales :
Couleur de fond (propriété backgroundColor).
Image de fond (propriété backgroundImage) : Utilisez ici des textures plutôt que des images (Opale.soya n'a pas pour l'instant de classe concrète d'image (opale.soya.soya2d.Image), mais les textures (interface opale.soya.soya2d.Texture, qui étend la précédente) en ont).
Brouillard (fog) : Le brouillard est de la même couleur que le fond de l'écran (backgroundColor). Il n'est effectif que si la prpriété fogEnabled est vrai. fogType, fogDensity (si fogType est exponentiel) ou fogStart, fogEnd (si fogType est linéaire) permettent de régler l'intensité du brouillard.
Notez que, pour l'instant, seule sont appliquées les propriétés de l'environnement contenant la caméra.
Opale.soya inclue un système d'animation capable d'interpoler à la volée entre deux états d'un world (et de tous les éléments graphiques qu'il contient : par exemple un personnage, ...).
Animation (opale.soya.soya3d.animation.Animation) : Une animation représente une suite d'états, chacun de ses états correspondant à un temps (time) donné.
Créer une animation : C'est très facile : il suffit de donner au world chacun de ces états, puis de les entrer dans l'animation (méthode addCurrentState(time, world)). Par exemple si vous voulez qu'au cours de l'animation un volume se déplace de (0, 0, 0) en (1, 0, 0) entre les temps 0 et 1, placez ce volume en (0, 0, 0) (méthode move(x, y, z)), applelez addCurrentState(0, monWorld), puis placez-le en (1, 0, 0) et appelez de nouveau addCurrentState(1, monWorld).
Jouer une animation : If faut tout d'abord définir l'animation du world (propriété animation), puis la propriété animationTime permet de faire jouer l'animation : l'état correspondant au temps demandé est alors appliqué au world. Dans l'exemple ci-dessus, monWorld.setAnimationTime(0) déplacera le volume en (0, 0, 0), monWorld.setAnimationTime(1) le déplacera en (1, 0, 0) et monWorld.setAnimationTime(0.5) le déplacera en (0.5, 0, 0).
Animation cyclique : Une animation est cyclique si elle doit se terminer en retournant à l'état de départ (ce qui permet de repartir pour un nouveau cycle). Dans ce cas, la propriété cyclicLap représente le temps séparant le dernier état ajouté du premier (si l'animation n'est pas cyclique, cyclicLap vaut 0).
Animations (opale.soya.soya3d.animation.Animations) : Animations est une collection d'animation (sans "s" !). Cet objet permet de grouper et de gérer aisément toutes les animation se rapportant à un même world (par exemple les animations "avancer", "reculer", "tourner", ... d'un perso). Les objets animations peuvent être enregistrer dans un répertoire prévu pour.
Si la propriété animations d'un world a été définie, la méthode setAnimationFromAnimations(name) du world permet de définir son animation depuis la collection animations, en récupérant l'animation correspondant au nom demandé.
La plupart des objets d'Opale.Soya (et notemment les éléments 3D) sont des javaBeans.
Attention ! Contrairement à ce que pense certains, les Beans ne se limitent absolument pas à des composants "visuels" type AWT ou Swing ! N'importe quel objet peut être (et est) un Bean (cf le package java.beans).
L'éditeur de Beans intégré (opale.soya.editor) permet d'éditer une liste de propriété à partir de n'importe quel bean, de la façon suivante :
Object bean = [...];
opale.soya.editor.Editor.edit(bean);
Opale.Soya utilise un système d'événements très proche de celui d'AWT et des javaBeans.
Tous les Beans disposent des méthodes addPropertyChangeListener et removePropertyChangeListener, permettant d'ajouter ou d'enlever un listener (java.beans.PropertyChangeListener). La méthode propertyChange du/des listener sera tout simplement appelée lorsqu'une propriété du bean sera modifiée (par une méthode set*(*)).
Evénements de déplacement, rotation ou redimensionnement (Move, Orientate, ResizeEvent): Si, par exemple, la propriété z d'un élément 3D graphique (GraphicalElement3D) est modifiée (par setX(float)), un événement (java.beans.PropertyChangeEvent) est créé et passé à la méthode propertyChange du/des listener, avec "z" pour nom de propriété (getPropertyName()).
Cependant, il est souvent plus intéressant de savoir que l'élément a été déplacé, plutôt que de savoir si c'est sa propriété x, y ou z qui a été modifiée. Dans ce cas, il suffit de tester si l'objet événement est une instance de opale.soya.soya3d.event.MoveEvent. De la même façon, il existe opale.soya.soya3d.event.OrientateEvent lorsqu'un objet est tourné, et opale.soya.soya3d.event.ResizeEvent lorsqu'il est redimensionné.
Evénements de collection (CollectionEvent) : Opale.Soya utilise l'interface opale.soya.util.Collection pour représenter les collections d'objets; cet interface étend l'interface java.util.Collection en ajoutant le support des événements lors de l'addition ou la suppression d'objets (add/remove event). Ces événements sont notemment utilisés par les world (qui implémentent opale.soya.util.Collection).
Sous-événements (SubEvent) : Les sous-événements sont les événements générés par les objets d'une collection, et non par cette collection elle-même. Wolrd3D permet d'accéder à des sous-événements pour tous les éléments ajoutés dedans. Cette fonction est récursive et s'applique même si le World3D n'est pas le parent direct de l'élément.
Pour ajouter un "listener" de sous-événement, utiliser la méthode addSubPropertyChangeListener ou addSubCollectionListener (selon le type d'événement désiré : événement de changement de propriété ou de collection), et les remove correspondants. Les "listeners" utilisés sont les mêmes que pour les événements "normaux".
Les tutoriels 5 et 6 traitent des événements.
La quasi-totalité des objets d'Opale.Soya peuvent être enregistrés. La sérialisation standard est utilisé pour tous les objets.
Certains objets sont enregistrés dans des fichiers à extensions exotiques (bien que le format soit toujours le même) :
.material | opale.soya.soya2d.Material |
.shape | opale.soya.soya3d.modeling.Shape |
.animations | opale.soya.soya3d.animation.Animations |
Opale.Soya intègre un grand nombre d'éditeurs (éditeurs de Beans ou 3D) !
Pour plus d'info sur les éditeurs, voir le Opale.Soya editor handbook (en anglais).
Opale.Soya comprend aussi une série de tutoriels dans opale.soya.tutorial (en anglais...).
Les leçons dont le numéro est inférieur à 100 concernent les bases, et les autres les effets spéciaux et autres éléments moins "importants".
Rassurez-vous cependant, il n'y a pas 100 leçons :-)
Il y aurait encore beaucoup à dire mais...
C'est fini !
Artiste on the Web (A11W)