jeudi 17 novembre 2011

Comment sauver et ouvrir les préférences du joueur dans Unity?

Il existe deux méthodes dont je connais l'existence pour enregistrer des données dans Unity:
  • la classe PlayerPrefs de Unity;
  • le XML.
PlayerPrefs est idéal pour enregistrer une petite quantité de données. La taille maximale permise est de 1 MO. Le site d'Unity pour cette classe est:
http://unity3d.com/support/documentation/ScriptReference/PlayerPrefs.html .
Le XML est plus versatile que PlayerPrefs. Vous pouvez enregistrer la quantité d’informations que vous voulez avec le format que vous voulez. Je vous réfère à ce site si vous voulez plus d'information sur le XML avec Unity:
http://answers.unity3d.com/questions/8480/how-to-scrip-a-saveload-game-option.html .

Dans ce tutoriel, je vais me concentrer uniquement sur PlayerPrefs. Cette classe offre trois fonctions pour mémoriser des données: SetFloat, SetInt et SetString et trois fonctions pour lire: GetFloat, GetInt et GetString. Disons que dans notre jeu, nous voulons sauver le nom du joueur qui est stocké dans la variable playerName, le code C# sera:
 PlayerPrefs.SetString( "PlayerName", playerName );
Le premier paramètre est la clé qui va permettre de retrouver la valeur et le second, la variable que nous voulons enregistrer. PlayerPrefs est un dictionnaire. Nous lui fournissons une valeur et une clé. Pour lire le nom du joueur, le code est simplement:
 playerName  = PlayerPrefs.GetString( "PlayerName");
Il n'y a rien de plus à faire. Unity se charge automatiquement d'enregistrer dans un fichier les valeurs de PlayersPrefs lors de la fermeture du jeu, que ce soit sur Windows ou en mode Web. Par contre, il est possible de forcer l'enregistrement à l'aide de la fonction:
 PlayerPrefs.Save();
Passons à un exemple concret pour voir comment appliquer PlayerPrefs dans une application. Voici un simple cube avec un bouton qui change sa couleur aléatoirement:
Unity Web Player | WebPlayer
-->

Le bouton change la couleur du cube. Si vous rafraîchissez la fenêtre, le cube réapparaît avec la dernière couleur choisie. Voici le script utilisé:
 
using UnityEngine;
using System.Collections;

/**
 * Change the color of a cube and save the data using PlayerPrefs.
 *
 * @author Mentalogicus
 * @date 11-2011
 * 
 * Copyright (C) 2011, Mentalogicus
 * All rights reserved
 * 
 */
public class StatsMenu : MonoBehaviour 
{
 //The current color of the cube.
 Color _current = new Color(1,1,1,1);

 // Use this for initialization
 void Start () 
 { 
  //Load the saved color.
  Load();
  
  //Change the shader to transparent diffuse.
  this.renderer.material.shader = Shader.Find("Transparent/Diffuse");
  
  //Apply the current color to our cube.
  this.renderer.material.color = _current;
 }
 
 /**
  * Show a button to change the color and 4 labels to show
  * the color RGBA component.
  */
 void OnGUI ()
 {
  GUI.BeginGroup( new Rect(50,50, 200,150) );
  GUI.Box( new Rect(0,0, 200,150), "Change Color" );
   
  if ( GUI.Button( new Rect( 10,25,100, 20), "Change Color" ) )
  {
   ChangeColor();
   AutoSave();
  }
  
  ShowColor();
  
  GUI.EndGroup();
 }
 
 /**
  * Show the current color used on your cube.
  */
 void ShowColor()
 {
  GUI.Label( new Rect( 10, 50, 150,20), "Red: " + _current.r );
  GUI.Label( new Rect( 10, 75, 150,20), "Green: " + _current.g );
  GUI.Label( new Rect( 10, 100, 150,20), "Blue: " + _current.b );
  GUI.Label( new Rect( 10, 125, 150,20), "Alpha: " + _current.a );
 }
 
 /**
  * Change the color to a random one.
  */
 void ChangeColor()
 {
  _current = new Color( Random.value, Random.value, Random.value, Random.value );
   
  this.renderer.material.color = _current;
 }
 
 /**
  * Save the color with the PlayerPrefs class.
  */
 void AutoSave()
 {
  PlayerPrefs.SetFloat( "Color_Red", _current.r );
  PlayerPrefs.SetFloat( "Color_Green", _current.g);
  PlayerPrefs.SetFloat( "Color_Blue", _current.b );
  PlayerPrefs.SetFloat( "Color_Alpha", _current.a );
 }
 
 /**
  * Load the color with the PlayerPrefs class.
  */
 void Load()
 {
  if ( PlayerPrefs.HasKey("Color_Red") )
  {
   _current.r = PlayerPrefs.GetFloat( "Color_Red" );
  }
  
  if ( PlayerPrefs.HasKey("Color_Green") )
  {
   _current.g = PlayerPrefs.GetFloat( "Color_Green");
  }
  
  if ( PlayerPrefs.HasKey("Color_Blue") )
  {
   _current.b = PlayerPrefs.GetFloat( "Color_Blue" );
  }
  
  if ( PlayerPrefs.HasKey("Color_Alpha") )
  {
   _current.a = PlayerPrefs.GetFloat( "Color_Alpha" );
  }
 }
 
}

Ce script est appliqué sur le cube. La classe PlayerPrefs est limitée dans ces capacités. Heureusement, il existe une extension nommée PlayerPrefsX qui étend les capacités de PlayerPrefs. Ce script est à l'adresse suivante:
http://www.unifycommunity.com/wiki/index.php?title=ArrayPrefs#C.23_-_PlayerPrefsX.cs .

Bonne programmation!

lundi 14 novembre 2011

Polyhedron Shower

Après avoir créé le code pour l’instanciation d'un cube, je n'ai pas pu m'arrêter et j'ai continué à ajouter du code. Voici le résultat:
-->

  • RandomObjectCreator.cs: crée les polygones à partir d'un tableau de Transform, les places de manières aléatoires et leurs donnent une vitesse et une rotation aléatoire.
  • RandomColorMaterial.cs: change la couleur et la transparence du matériel. Les belles couleurs viennent de se simple script!
  • Timer.cs: C'est une classe utilitaire qui indique si le délai est écoulé en appelant la fonction trigger.
  • PointLightAtCenter.cs: ce script ajoute une lampe au centre de mes polyèdres. Ce sont ces petites lampes qui ajoutent l'ambiance et font ressortir les couleurs.
  • BounceBack.cs: ce script inverse la vitesse des polyèdres lorsqu'il dépasse une certaine distance du centre.
RandomObjectCreator.cs

using UnityEngine;
using System.Collections;

/**
 * Choose randomly a polyhedron and instantiate it at a random position with
 * a random rotation. After that, add a RigidBody and assign a random force and
 * a random torque to move and rotate the polyhedron.
 *
 * @author Mentalogicus
 * @date 11-2011
 * 
 * Copyright (C) 2011, Mentalogicus
 * All rights reserved
 * 
 */
public class RandomObjectCreator : MonoBehaviour 
{
 
 
 public Transform[] _prefab;
 public float _force = 10;
 public float _radius = 10;
 
 // Use this for initialization
 void Start () 
 {
  
 }
 
 // Update is called once per frame
 void Update () 
 {
  
  if ( Input.GetKey("space") )
  {
   //Create a random position.
   //The insideUnitSphere return a random position inside a sphere of radius 1.
   Vector3 rndPos = Random.insideUnitSphere * 10;
   
   //Create a random rotation.
   Quaternion rndRotation = Random.rotation;
   
   //Choose randomly between the array of object
   int choice = (int) Mathf.Floor(Random.Range( 0.0f, _prefab.Length -.0000001f ) );
   
   //Instantiate a new object at a random position with a random rotation.
   Transform newGameObj = Instantiate( _prefab[choice], rndPos, rndRotation) as Transform; 
   
   //Rename the game object.
   newGameObj.name = "Allo";
   
   //Add a rigid body to the object.
   newGameObj.gameObject.AddComponent();
   
   //Remove the gravity.
   newGameObj.gameObject.rigidbody.useGravity = false;
   
   //Change the mass of the object to 1.
   newGameObj.gameObject.rigidbody.mass = 1.0f;
   
   //Add a random impulse to the rigidbody.
   newGameObj.gameObject.rigidbody.AddForce(new Vector3( Random.Range(-_force,_force), 
                                                         Random.Range(-_force,_force), 
                                                         Random.Range(-_force,_force) ), 
                                                 ForceMode.Impulse);
   
   //We add a random torque to the rigidbody to make him rotate!!!
   newGameObj.gameObject.rigidbody.AddTorque(new Vector3( Random.Range(-_force,_force), 
                                                         Random.Range(-_force,_force), 
                                                         Random.Range(-_force,_force) ), 
                                                 ForceMode.Impulse);
  }
  else if ( Input.GetKey("q") )
  {
   Application.Quit();
  }
  else if ( Input.GetKeyDown("return") )
  {
   GameObject[] objs = GameObject.FindGameObjectsWithTag ("Block");
   foreach ( GameObject obj in objs)
   {
    Destroy(obj);
   }
  }
 }

}

Licence Creative Commons
RandomObjectCreator.cs de Mentalogicus est mis à disposition selon les termes de la licence Creative Commons Paternité - Pas de Modification 3.0 non transposé.  

RandomColorMaterial.cs

using UnityEngine;
using System.Collections;

/**
 * Randomly assign a color and an alpha to the material of the object.
 * 
 *
 * @author Mentalogicus
 * @date 11-2011
 * 
 * Copyright (C) 2011, Mentalogicus
 * All rights reserved
 * 
 */
public class RandomColorMaterial : MonoBehaviour
{

 
 // Use this for initialization
 void Start ()
 {
  
  Shader shader1 = Shader.Find("Transparent/Diffuse");
  
  this.renderer.material.shader = shader1;
  
  //Set the main color of the material
  this.renderer.material.color = CreateRandomColor();
  
 }
 
 private Color CreateRandomColor()
 {
  float r = Random.value;
  float g = Random.value;
  float b = Random.value;
  float a = Random.Range(.4f,1f);
  
  return new Color( r, g, b, a );
 }
}
Licence Creative Commons
RandomColorMaterial.cs de Mentalogicus est mis à disposition selon les termes de la licence Creative Commons Paternité - Pas de Modification 3.0 non transposé.

Timer.cs

using System;
using UnityEngine;

/**
 * Timer with a delay. When the delay is over by, the trigger properties will
 * return true.
 *
 * @author Mentalogicus
 * @date 11-2011
 * 
 * Copyright (C) 2011, Mentalogicus
 * All rights reserved
 * 
 */
namespace Utils
{
 public class Timer
 {
  private float _timer;
  private float _delay;
  
  
  public void Start(  )
  {
   this.Reset();
  }
  
  public void Start( float delay )
  {
   _delay = delay;
   this.Reset();
  }
  
  public float time
  {
   set { _timer = value; } 
  }
  
  public float delay
  {
   get { return _delay; }
   set { _delay = value; }
  }
  
  public void Reset()
  {
   _timer = Time.time; 
  }
  
  public bool overBy
  {
   get { return (Time.time - _timer) >= _delay; }
  }
  
  public bool underBy
  {
   get { return (Time.time - _timer) < _delay; }
  }
  
  /**
   * When called, this properties return if the timer is overBy or underBy and
   * reset the timer.
   */
  public bool trigger
  {
   get
   {
    if ( overBy )
    {
     Reset();
     return true; 
    }
    else
    {
     return false;
    }
   }
  }
  
 } //End class
} //End namespace


Licence Creative Commons
Timer.cs de Mentalogicus est mis à disposition selon les termes de la licence Creative Commons Paternité - Pas de Modification 3.0 non transposé.
 
PointLightAtCenter.cs

using UnityEngine;
using System.Collections;

/**
 * Add a point like in the center of an object.
 *
 * @author Mentalogicus
 * @date 11-2011
 * 
 * Copyright (C) 2011, Mentalogicus
 * All rights reserved
 * 
 */
public class PointLightAtCenter : MonoBehaviour
{
 public float _range = 5f;
 
 // Use this for initialization
 void Start ()
 {
  //Add a point light in the center of the object.
  Light spot = this.gameObject.AddComponent();
  spot.type = LightType.Point;
  spot.range = _range;
  spot.intensity = 2;
 }

}


Licence Creative Commons
PointLightAtCenter.cs de Mentalogicus est mis à disposition selon les termes de la licence Creative Commons Paternité - Pas de Modification 3.0 non transposé.
 
BounceBack.cs
 
using UnityEngine;
using System.Collections;
using Utils;

/**
 * Inverse the velocity of an object that is at a distance greater
 * than the specified radius.
 *
 * @author Mentalogicus
 * @date 11-2011
 * 
 * Copyright (C) 2011, Mentalogicus
 * All rights reserved
 * 
 */
public class BounceBack : MonoBehaviour 
{
 
 public float _radius = 50;
 
 private Timer _timeToReturnBack = new Timer( );
 
 void Start()
 {
  _timeToReturnBack.Start( .2f );
 }
 
 // Update is called once per frame
 void Update () 
 {
  if ( this.transform.position.x * this.transform.position.x +
       this.transform.position.y * this.transform.position.y +
       this.transform.position.z * this.transform.position.z > _radius * _radius &&
       _timeToReturnBack.trigger )
  {
   this.rigidbody.velocity = -this.rigidbody.velocity;
  }
 }
}

Licence Creative Commons
BounceBack.cs de Mentalogicus est mis à disposition selon les termes de la licence Creative Commons Paternité - Pas de Modification 3.0 non transposé.

Comment instancier des objets dans Unity 3D?

Unity 3D est un engin de jeux complets avec un éditeur. Si Unity vous est totalement inconnu, je vous recommande de visiter leur site: http://unity3d.com/. Ce tutoriel assume que vous avez déjà un peu joué avec Unity 3D et que vous êtes familier avec l'interface. Les scripts sont en C#. Le but du tutoriel est d'apprendre à instancier des objets durant l'exécution du jeu et non dans l'éditeur.
L'éditeur d'Unity.
La première étape consiste à créer un nouveau projet dans Unity. Je l'ai appelé "RandomObjectsCreator". À la création du projet, Unity crée les répertoires Assets, Library et Temp. Le répertoire Assets sert de librairie pour les objets que nous ajoutons aux jeux. C'est le seul des trois répertoires que nous allons utiliser.

L'instanciation d'objet dans Unity se fait à l'aide de prefabs. Prefab est la version raccourcie de Préfabriquée. Le prefab est l'équivalent d'un plan de construction. L'objet a telle forme, telle couleur et va réagir de telle et telle façon dans telles et telles conditions. Une fois le prefab créé, il est possible de créer des milliers de versions du même objet à l'intérieur du jeu et chaque objet aura la forme et le comportement spécifié dans le prefab.

Pour créer un prefab, aller dans l'onglet Projet->click droit->Create->Prefab. Renommer ce prefab "Cube".
Menu pour la création d'un prefab.
L'icône du prefab est une boîte blanche vide. Cette icône indique que le prefab est vide. Pour le remplir, nous allons ajouter un cube à la scène. Dans le menu supérieur: GameObject->Create Other->Cube.
Création d'un cube dans Unity.
Dans le menu "Hierarchy", sélectionnez Cube et glissez le sur le prefab Cube. L'icône du prefab devient une boîte bleue, indiquant que le prefab contient un objet. Vous pouvez maintenant effacer le Cube de la scène. Si vous glissez le prefab Cube sur la scène, un cube apparaît! Magika!
Ajout d'un objet au prefab.
Il est temps de créer notre script. Dans le tab "Project", click droit->Create->C# Script. Nommons-le: "RandomObjectsCreator". En double cliquant sur le script, l'éditeur par défaut de Unity ouvre le script. Dans mon cas, c'est MonoDevelop qui est ouvert. Unity a créé automatiquement une classe nommée RandomObjectsCreator avec la fonction Start et la function Update:

using UnityEngine;
using System.Collections;

public class RandomObjectsCreator : MonoBehaviour 
{

 // Use this for initialization
 void Start () 
 {
 
 }
 
 // Update is called once per frame
 void Update () 
 {
 
 }
}

La classe hérite de la classe générique MonoBehavior. Les scripts attachés sur des objets dans la scène doivent obligatoirement hériter de MonoBehavior pour fonctionner. La classe MonoBehavior permet d'accéder aux propriétés et aux composants de l'objet auquel est attaché le script.

Le code pour instancier un prefab est:
Instantiate( _prefab, position, rotation);
La fonction Instantiate va créer un clone du prefab contenu dans la variable _prefab, l'afficher dans la scène à la position "position" avec la rotation "rotation" et va retourner une référence de l'objet avec le type Object. Le type Object est le type de base en C#. Dans le précédent code, nous n'avons pas mis de variable pour récupérer la référence à l'objet. Pour ce faire, le code sera:
Transform newGameObj = 
     Instantiate( prefab, position, rotation) as Transform; 
Transform est un type générique d'Unity qui permet d'accéder à toutes les propriétés de l'objet de la scène. Le as Transform est un casting. Instantiate retourne une référence de type Object. Or, nous voulons une référence de type Transform. Nous indiquons la transformation de la référence de type Object en une référence de type Transform avec le as Transform. Il est à remarquer que cette notation est identique à la suivante:
Transform newGameObj = 
     (Transform)Instantiate( _prefab, position, rotation); 
Personnellement, je préfère le as, car c'est plus proche de l'anglais que l'opérateur de casting avec parenthèse.

Pour faire apparaître un cube, le joueur doit peser sur la touche espace. Le code est entré dans la fonction Update qui est appelée chaque fois que l'écran est rafraichi. La commande nécessaire est:
Input.GetKey("space");
Cette fonction retourne vrai si la touche espace est enfoncée et faux autrement. La position de notre cube sera choisi aléatoirement dans une sphère d'un rayon déterminé par la variable _radius:
Vector3 rndPos = Random.insideUnitSphere * _radius;
et avec une rotation aléatoire:
Quaternion rndRotation = Random.rotation;
Le script complet est le suivant:
using UnityEngine;
using System.Collections;

public class RandomObjectsCreator : MonoBehaviour 
{
 public Transform _prefab;
 public float _radius = 10;
 
 // Update is called once per frame
 void Update () 
 {
  if ( Input.GetKey("space") )
  {
   //Create a random position.
   //The insideUnitSphere return a random position inside a sphere of radius 1.
   Vector3 rndPos = Random.insideUnitSphere * _radius;
   
   //Create a random rotation.
   Quaternion rndRotation = Random.rotation;
   
   //Instantiate a new object at a random position with a random rotation.
   Transform newGameObj = Instantiate( _prefab, rndPos, rndRotation) as Transform; 
  }
 }
}

Ce script doit être attaché à un GameObject dans la scène pour fonctionner. Nous allons créer un GameObject vide. Dans le menu GameObject->Create Empty.Un objet nommé GameObject est crée. Renommons le ObjectsCreator. Sélectionner ObjectsCreator et glisser le script RandomObjectsCreator dans l'onglet Inspector. Le script contient deux variables prefab, radius. Glisser le prefab Cube sur le none à droite de la variable prefab. Le nom Cube apparaît avec une icône de manipulateur 3D.
Ajout d'un script et assignation de la variable prefab aux prefab Cube.
Appuyer sur Play et ensuite sur la touche "espace". Des cubes vont apparaître aléatoirement à l'écran. Je vous recommande d'ajouter une lampe directionnelle dans la scène pour embellir votre rendu.
-->

Comment insérer un jeu Unity dans Blogger?

Lorsque je pose cette question sur Internet, je me retrouve sur des sites qui proposent des Gadgets à ajouter à Blogger. J'ai essayé plein de combinaisons et rien ne marche. Au final, la solution est simple. Lorsque vous compilez votre jeu sous Unity pour le Web, Unity crée un fichier .unity3d et un fichier .html. Le fichier .html contient ceci:
 
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
 <head>
  <title>Unity Web Player | WebPlayer</title>
  <script type="text/javascript" src="http://webplayer.unity3d.com/download_webplayer-3.x/3.0/uo/UnityObject.js"></script>
  <script type="text/javascript">
  <!--
  function GetUnity() {
   if (typeof unityObject != "undefined") {
    return unityObject.getObjectById("unityPlayer");
   }
   return null;
  }
  if (typeof unityObject != "undefined") {
   unityObject.embedUnity("unityPlayer", "WebPlayer.unity3d", 680, 550);
   
  }
  -->
  </script>
  <style type="text/css">
  <!--
  body {
   font-family: Helvetica, Verdana, Arial, sans-serif;
   background-color: white;
   color: black;
   text-align: center;
  }
  a:link, a:visited {
   color: #000;
  }
  a:active, a:hover {
   color: #666;
  }
  p.header {
   font-size: small;
  }
  p.header span {
   font-weight: bold;
  }
  p.footer {
   font-size: x-small;
  }
  div.content {
   margin: auto;
   width: 550px;
  }
  div.missing {
   margin: auto;
   position: relative;
   top: 50%;
   width: 193px;
  }
  div.missing a {
   height: 63px;
   position: relative;
   top: -31px;
  }
  div.missing img {
   border-width: 0px;
  }
  div#unityPlayer {
   cursor: default;
   height: 550px;
   width: 550px;
  }
  -->
  </style>
 </head>
 <body>
  <p class="header"><span>Unity Web Player | </span>WebPlayer</p>
  <div class="content">
   <div id="unityPlayer">
    <div class="missing">
     <a href="http://unity3d.com/webplayer/" title="Unity Web Player. Install now!">
      <img alt="Unity Web Player. Install now!" src="http://webplayer.unity3d.com/installation/getunity.png" width="193" height="63" />
     </a>
    </div>
   </div>
  </div>
  <p class="footer">« created with <a href="http://unity3d.com/unity/" title="Go to unity3d.com">Unity</a> »</p>
 </body>
</html> 
Effacer les styles css, ils vont bousiller la mise en page de votre page. Dans Blogger, ouvrer un nouveau message et aller dans Modifier le code HTML. Coller ce code HTML (sans les styles). Une icône de Unity va apparaître, mais pas le jeu. Dans le code HTML, à l'endroit où est écrit:
if (typeof unityObject != "undefined") {
     unityObject.embedUnity("unityPlayer", "WebPlayer.unity3d", 680, 550);
}
remplacez "WebPlayer.unity3d" avec le lien Web où est hébergé votre jeu. Bon, si vous êtes comme moi, vous n'avez pas vraiment d'hébergeur, alors quoi faire? J'utilise Google Sites pour ce faire. Je me suis créé une page Web nommée FractalGameLab. Je vais dans Google Sites, dans les outils de gestion et j'importe le .unity3d. Je copie ensuite le lien pour télécharger la ressource et je remplace WebPlayer.unity3d par ce lien. Voilà! Le tour est joué. Pour prouver que ça marche, voici une démo de jeu que j'ai fait avec le livre de "Développez des jeux 3D avec Unity" par Will Goldstone.
Unity Web Player | WebPlayer
-->
Unity Web Player | WebPlayer

Quels sont les bons sites pour apprendre Blender?

Blender 3D est un logiciel Open Source de modélisation 3D. Ce logiciel est extrêmement puissant et permet de réaliser de superbes images de synthèse. De plus, il est possible d'y ajouter des greffons en Python et de programmer ces propres greffons! Pour vous mettre en appétit, voici un rendu que j'ai créé à partir d'un tutoriel avec Blender:
"Shaken, not stirred" martini dans Blender.

De tout les logiciels que j'ai utilisé dans ma vie, Blender est le plus difficile à apprendre. La difficulté principale consiste à apprendre les innombrables raccourcis clavier et de les utiliser dans le bon ordre. Blender utilise les trois boutons de la souris en combinaison des touches Ctrl, Shift et Alt. Au début, c'est extrêmement mêlant. Simplement bouger un cube est une tâche héroïque. Les raccourcis et les menus changent dépendant du contexte! Apprendre Blender est un peu comme réapprendre à taper au clavier. C'est difficile au début, mais une fois appris, le récompense est immense. Dans le cas de Blender, la récompense est une productivité considérablement accrue.

Pour débuté Blender, je vous recommande fortement de suivre la série de tutoriels par Neal Hirsig. Je dirais même que pour un débutant, ce tutoriel est essentiel et incontournable. Ces tutoriels ont une superbe progression commençant par la base de la manipulation des objets jusqu'à l'ajout des textures et des effets de particules. La coupe de Martini que j'ai créée vient d'un de ces tutoriels.

Par la suite, je recommande d'écouter les vidéos sur Blender Cookie. Le site possède des tutoriels sur la modélisation, la création de texture, la création de modèles pour Unity 3D, le scipting en Python dans Blender, etc. Le site est une ressource fantastique pour le débutant, tout comme pour le modeleur plus avancé.

Bonne modélisation à tous!