Au fil des flows

1avr/130

Jouons avec la physique : Simulateur de tissu

Posted by Fabien Arcellier

Il y'a peu, je suis tombé sur une démonstration HTML5 où l'on manipule un tissu à l'aide de la souris. J'ai été surpris par un fait, outre que ce soit du JavaScript, le code source de cet exemple faisait moins de 300 lignes.

Surprenant, comment en 350 lignes, ce code exécute cette simulation avec une telle fluidité ?
Au final, j'ai trouvé quelques réponses dans les commentaires de Reddit. Cet exemple utilise pour ses calculs l'algorithme Intégration de Verlet et un système de contrainte assez simple.

Voulant comprendre le fonctionnement de ce code, je l'ai porté en C++ en utilisant le framework Qt. Cette implémentation n'est pas optimisée. Par exemple, je traite les coordonnées x et y comme 2 champs de structure là où je pourrai les traiter sur la forme d'un tableau 2D. Il est possible qu'à l'avenir, je refactore ce code pour pouvoir utiliser cet exemple dans un environnement 3D comme Openglide. Je suis conscient que le code reste très monolithique. Le calcul et le rendu se font dans les mêmes classes.

Vous pouvez télécharger le code de ce portage sur GitHub.

Integration de Verlet

Comment transmettre le mouvement ? Pour ceux qui ont quelques restes de mécanique, vous pensez de suite, Newton, vecteur, application d'une ou plusieurs forces, accélération, intégrales, équation différentielle ... De suite ça parait très compliqué.

Il existe un algorithme offrant une facilité d'utilisation pour le calcul de déplacement. Cet algorithme c'est l'intégration de Verlet.
Au lieu de s'appuyer sur une équation différentielle position actuelle, vitesse et accélération, cet algorithme s'appuie sur une équation différentielle ayant trois arguments la position actuelle, la position précédente et l'accélération.

/*!
 * Methode qui calcule la nouvelle position du point en s'appuyant sur l'intégration de Verlet
 */
void Point::Update(qreal delta)
{
    // On applique la gravité
    this -> AddForce(0, this -> m_gravity * this -> m_mass);

    qreal vx = this -> m_x - this -> m_px;
    qreal vy = this -> m_y - this -> m_py;

    // Verlet integration equation
    delta *= delta;
    qreal nx = this -> m_x + vx + this -> m_ax * delta;
    qreal ny = this -> m_y + vy + this -> m_ay * delta;

    this -> m_px = this -> m_x;
    this -> m_py = this -> m_y;
    this -> m_x = nx;
    this -> m_y = ny;

    this -> m_ax = 0;
    this -> m_ay = 0;
}

Comment la souris interagit t'elle avec le tissu ?

Dans la méthode UpdateMouse, l'algorithme d'origine compare la position actuelle de la souris avec la position précédente et se sert de cette différence pour établir une vitesse. Est ce une bonne vision de la réalité ?

J'ai fait un choix légèrement différent du code d'origine, finissant sur la même conclusion.

J'utilise la position précédente et actuelle de la souris pour appliquer une accélération :

this -> AddForce((this -> m_mouse -> x - this -> m_mouse -> px) * 1000,
                             (this -> m_mouse -> y - this -> m_mouse -> py) * 1000);

Pourquoi 1000 ? La force de la gravité est fixée à 900 dans ce modèle (normalement, 9,81m/s²). J'ai choisi un coefficient qui avait un impact. Je ne pourrai pas expliquer ce choix en m'appuyant sur la mécanique.

On résout l'effet de cette force au travers de l'intégration de Verlet.

void Point::UpdateMouse()
{
    Q_CHECK_PTR(this -> m_mouse);
    if (this -> m_mouse -> down == false)
    {
        return;
    }

    qreal diff_x = this -> x() - this -> m_mouse -> x;
    qreal diff_y = this -> y() - this -> m_mouse -> y;
    qreal distance = qSqrt(diff_x * diff_x + diff_y * diff_y);
    if (this -> m_mouse -> button == Qt::LeftButton)
    {
        if (distance < this -> m_mouseInfluence)
        {
            this -> AddForce((this -> m_mouse -> x - this -> m_mouse -> px) * 1000,
                             (this -> m_mouse -> y - this -> m_mouse -> py) * 1000);
        }
    }
    else if (distance < this -> m_mouseCut)
    {
        this -> ClearConstraint();
    }
}

J'ai rajouté quelques interactions au clavier (sur les lettres Q,Z,D,S) :

void Point::UpdateKeyboard()
{
    if (this -> m_keyboard->keyup_down)
    {
        this -> AddForce(0, -2000);
    }

    if (this -> m_keyboard-> keydown_down)
    {
        this -> AddForce(0, 2000);
    }

    if (this -> m_keyboard -> keyright_down)
    {
        this -> AddForce(2000, 0);
    }

    if (this -> m_keyboard -> keyleft_down)
    {
        this -> AddForce(-2000, 0);
    }
}

Resolution des contraintes entre les mailles

La résolution de contrainte se fait dans la classe Constraint. Voici l'illustration de ce qui se passe :

void Constraint::Solve()
{
    qreal diff_x = this -> m_p1 -> x() - this -> m_p2 -> x();
    qreal diff_y = this -> m_p1 -> y() - this -> m_p2 -> y();
    qreal distance = qSqrt(diff_x * diff_x + diff_y * diff_y);
    qreal diff_distance = (this -> m_length - distance)/distance;


    // Il ne devrait jamais y avoir rupture (800 - 30)/(30)
    if (diff_distance > this -> m_tearDistance)
    {
        this -> m_p1 -> RemoveConstraint(this);
    }

    // scalar_1 is the ratio from p1 mass in comparaison with the p1 mass + p2 mass
    qreal scalar_1 = ((1 / this -> m_p1 -> mass()) / (1 / this -> m_p1 -> mass() + 1 / this -> m_p2 -> mass()));
    qreal scalar_2 = 1 - scalar_1;

    // Find a way to use directly arithmetic operation
    qreal x1_new = this -> m_p1 -> x() + diff_x * scalar_1 * diff_distance;
    this -> m_p1 -> setX(x1_new);
    qreal y1_new = this -> m_p1 -> y() + diff_y * scalar_1 * diff_distance;
    this -> m_p1 -> setY(y1_new);

    qreal x2_new = this -> m_p2 -> x() - diff_x * scalar_2 * diff_distance;
    this -> m_p2 -> setX(x2_new);
    qreal y2_new = this -> m_p2 -> y() - diff_y * scalar_2 * diff_distance;
    this -> m_p2 -> setY(y2_new);
}

Conclusion

Je ne connaissai pas ces algorithmes. Ce sont des outils surprenants, je les imaginai bien plus complexe. Ils n'ont pas vocation à faire de la simulation physique mais offre des possibilités surprenantes. J'ai la sensation d'être devant un univers immense et j'hésite où poser le pied après.

En savoir plus

Remplis sous: Non classé Aucun commentaire
9jan/130

Les erreurs de compilation avec Mscvc 2010 sous Qt Creator

Posted by Fabien Arcellier

Voici une liste non exhaustive d'erreurs de compilation (ou de linkage) associées à leur résolution que j'ai rencontré ces derniers mois en développant une application sous QT.

  • C2011 : Verifier l'include guard dans les fichiers headers (l'un d'entre eux peut etre mauvais)
  • C2504 : Verifier l'include guard dans les fichiers headers (l'un d'entre eux peut etre mauvais)
  • C2664 : Inheritance hides overriding (http://www.parashift.com/c++-faq-lite/hiding-rule.html)
  • LN2019 : Il peut rester des fichiers sur le disque portant le meme nom (réaliser une recherche)
  • LNK2001 : Reexecuter qMake
  • C1083 : Ouvrir le fichier pro et commenter la ligne QT -= gui
  • U4004 : Verifier les fichiers pri et pro, si on utilise bien *= pour l'assignation des variables
  • U1073 : Vérifier le chemin de votre projet (il est peut etre trop grand, limite de NMake sur windows)

Les solutions à ces erreurs sont peut être valable que dans notre contexte. Si vous les rencontrez et que vous trouvez d'autres solutions, n'hésitez pas à le mettre en commentaire.

Mis à jour le 05/04/2013, Ajout de l'erreur U1073

Remplis sous: Non classé Aucun commentaire
4avr/121

Ecrire plusieurs classes de tests dans un meme projet avec QT

Posted by Fabien Arcellier

J'aurai préféré posté la partie 2 de l'analyser d'equation mais j'ai un peu de retard dans sa mise en oeuvre. Je poste la résolution d'un problème que j'ai rencontré cette semaine avec Qt et la bibliotheque QTestLib.

Lorsque l'on crée un projet de test unitaire avec QT Creator, il crée une seule classe de test unitaire agrégée dans un fichier cpp.

Vue de QtCreator

Vue de QtCreator

A première vue, QT n'offre pas d'assistant pour créer de nouvelles classes de tests. C'est surprenant. Perdre cette dualité classe - classe de tests est un problème. En faite, si vous voulez composer une suite de tests unitaires comme vous feriez avec un framework de type xUnit, il faut apporter quelques modifications au projet. Pour cela commençons par comprendre comment est structuré le système.

Comment ça marche ?

Dans notre projet de test unitaire, il n'y a qu'un fichier cpp découpé en 3 parties :

  • la définition de la classe Modele1TestsTest
  • L'implémentation de la classe Modele1TestsTest
  • La macro QTEST_APPLESS_MAIN(Modele1TestsTest) + l'include du fichier moc

A première vue, le modele est proche de xUnit. On a une classe de test, une methode de type setup et teardown qui sont exécutés entre chaque test (respectivement initTestCase et cleanupTestCase).

Regardons de plus près la macro QTEST_APPLESS_MAIN. Vous trouverez sa définition dans les sources de QT dans le dossier src\testlib\qtest.h.

#define QTEST_APPLESS_MAIN(TestObject) \
int main(int argc, char *argv[]) \
{ \
    TestObject tc; \
    return QTest::qExec(&tc, argc, argv); \
}

Cette macro est simplement un raccourci pour déclarer un main. Je dois avouer que faire usage d'une macro pour ça est assez surprenant à mon goût. Pour économiser 5 lignes de code, on perd complètement la compréhension du système et donc l'impossibilité de l'étendre. Donc si on suit la logique, il suffit pour tester plusieurs classes de supprimer l'appel à cette macro et d'écrire une fonction main qui appelle les objets de tests un à un en utilisant QTEST::qExec(...).

Il reste l'include du fichier moc à comprendre. En faite, sa présence vient du fait qu'il n'y a pas de fichier header. Plus d'explications sont fournis dans la FAQ de developpez.com.

Si il y'avait un fichier header, on pourrait s'en passer. gMake se chargeant de faire le linkage.

Une demarche pour implementer une suite de tests

  • Creer un fichier main.cpp qui contient la fonction main pour appeller la suite de tests unitaires
  • Déplacer la définition de la classe Modele1TestsTest dans un fichier header
  • Supprimer La macro QTEST_APPLESS_MAIN(Modele1TestsTest) + l'include du fichier moc

Vous aurez des inclusions à faire pour que tout fonctionne mais rien de trop sorcier. A ce stade, vous avez le meme résultat que précedemment. Maintenant vous pouvez rajouter une classe de test supplémentaire.

Il suffira de créer un main de la forme suivante :

int main( int argc, char *argv[])
{
    Modele1TestsTest tc ;
    QTest::qExec (&tc, argc, argv );
    Modele2Tests tc2 ;
    QTest::qExec (&tc2, argc, argv );
    return 0;
}

A présent vous retrouvez le comportement plus habituel des frameworks de tests.

Remplis sous: Non classé 1 commentaire
2avr/120

Concatener des chaines de caracteres statiques en C++

Posted by Fabien Arcellier

Il est possible de concatener des chaines de caracteres statiques en C++ de plusieurs façon.

Le caractere de poursuite de ligne \ :

QLabel *label = new QLabel("<h2><em>Hello</em></h1> \
                            <font color=red>Qt</font></h2>" );

Le caractere de poursuite de ligne est classique. Il existe aussi en C. On l'utilise fréquemment dans les définitions de macros. La ligne suivante commence au premier caractere différent de l'espace ou tabulation.

Plusieurs chaines à la suite

QLabel *label = new QLabel("<h2><em>Hello</em></h1>"
                         "<font color=red>Qt</font></h2>" );

Il suffit de déclarer à chaque ligne une nouvelle chaine de caracteres.
Celle ci est automatiquement concaténée à la précédente.

Remplis sous: Non classé Aucun commentaire