Aller au contenu

TP — Structures et raytracer (partie 1)

Travaux pratiques · MP2I · 2021-2022 · C

Cours : Programmation impérative

Vous êtes libres de la structure de vos fichiers pour ce TP mais si vous vous sentez perdus, voici une marche à suivre plus guidée :

  • ouvrir un terminal :
    • mkdir tp06
    • cd tp06
  • créer un fichier tp06.c qui contient une fonction main rudimentaire ;
  • on va créer des fichiers autour et compiler le programme ; à défaut du Makefile distribué à l'époque, on peut écrire gcc -Wall -g -o tp06 *.c -lm (l'option -g compile en mode debug) ; pour l'exécuter ce sera juste ./tp06.

Structures

Il est possible de définir ses propres structures en C qui agrègent d'autres types. Ainsi, on pourra définir une structure point permettant de représenter des points du plan :

struct point {
    double x;
    double y;
};

On peut alors définir un élément de cette structure avec le type struct point et on peut accéder à ses champs par l'utilisation de .. Pour obtenir le point \((3,4)\) on écrit :

struct point p;
p.x = 3;
p.y = 4;

On peut passer en paramètre une telle structure et la renvoyer en valeur de retour comme ici :

struct point cree_point(double x, double y)
{
    struct point p;
    p.x = x;
    p.y = y;
    return p;
}

void affiche_point(struct point p)
{
    printf("(%f,%f)\n", p.x, p.y);
}

struct point milieu(struct point a, struct point b)
{
    struct point m;
    m.x = (a.x + b.x)/2;
    m.y = (a.y + b.y)/2;
    return m;
}

// exemple
struct point p1 = cree_point(0, 10);
struct point p2 = cree_point(6, 4);
affiche_point(milieu(p1, p2));

On remarque que c'est un peu pénible d'écrire struct point partout ; il est possible d'utiliser un typedef pour définir un type synonyme. Si jamais on a une structure struct t_s, on peut écrire typedef struct t_s t; pour pouvoir utiliser t à la place. On peut même avoir le même nom comme ici :

typedef struct point point;

point p;
p.x = 2;
p.y = 3;
affiche_point(p);

Quand on sépare en .c et .h, il faut mettre les types et les déclarations (ou prototypes) dans le .h et les définitions dans le .c. Par exemple, on pourrait écrire un fichier point.h contenant

struct point {
    double x;
    double y;
};

typedef struct point point;

point cree_point(double x, double y);

puis un fichier point.c contenant

#include "point.h"

point cree_point(double x, double y)
{
    point p;
    p.x = x;
    p.y = y;
    return p;
}

Raytracer - Partie 1

Je vous demande ici de créer une bibliothèque complète permettant de travailler sur des vecteurs (ou de manière équivalente des points) dans l'espace. On s'en servira pour construire, au fur et à mesure des TP, un raytracer.

Je vous donne le type :

struct vec3 {
    double x;
    double y;
    double z;
};

typedef struct vec3 vec3;

et vous devez faire une série de fonctions complètes :

  • vec3_create qui crée un tel vecteur ;
  • vec3_add qui renvoie la somme de deux vecteurs ;
  • vec3_diff qui renvoie la différence de deux vecteurs/points ;
  • vec3_dot qui renvoie le produit scalaire de deux vecteurs ;
  • vec3_cross qui renvoie le produit vectoriel de deux vecteurs (go go Wikipédia) ;
  • vec3_smul tel que vec3_smul(a, v), où a est un double et v un vecteur, renvoie le vecteur \(a \vec v\) (comme \(2\vec v\), \(-3.5 \vec v\), …) ;
  • vec3_norm qui renvoie la norme d'un vecteur ; on pourra utiliser sqrt déclaré dans math.h pour la racine ;
  • vec3_normalize qui prend un vecteur \(\vec v\) et renvoie \(\frac{\vec v}{\|\vec v\|}\) si \(\vec v\) est non nul, sinon on renvoie un vecteur de norme quelconque comme \((1,0,0)\).

Je vous demande que l'en-tête soit vec3.h et le code dans vec3.c. Chaque déclaration dans le .h devra avoir un en-tête de documentation sous la forme :

/*
 * nomdefonction : description de ce qu'elle fait
 *                 et de ce qu'elle renvoie.
 *
 * arg1 : role de l'argument 1
 * arg2: role de l'argument 2
 * ...
 */

Je vous demande aussi de tester vous-même chaque fonction et de placer le code dans vec3_test.c ; il doit contenir une fonction principale test qui lance tous les tests que vous aurez écrits, chacun de préférence dans des fonctions séparées :

void test_fonction1()
{
    ...
}

void test_fonction2()
{
    // ...
}

void test()
{
    test_fonction1();
    test_fonction2();
    // ...
}

Dans vos tests, utilisez assert comme dans assert(mafonction() == resultat_attendu); qui est défini dans assert.h et vous permet de vérifier que les fonctions font ce qu'on attend d'elles.

Vous pouvez faire un fichier vec3_test.h qui expose la fonction de test principale et l'appeler dans main.

Le contenu de vec3_test.h sera alors juste

void test();

et celui de tp06.c sera

#include "vec3_test.h"

int main()
{
    test();
    return 0;
}

Question 1

Écrire le fichier vec3.h.

Corrigé
struct vec3 {
    double x;
    double y;
    double z;
};

typedef struct vec3 vec3;

/* vec3_create : renvoie un vec3 initialisé
 *               avec les paramètres
 *
 * x, y, z : coordonnées du vec3
 */
vec3 vec3_create(double x, double y, double z);

/* vec3_add : renvoie la somme de deux vecteurs
 *            la valeur renvoyée est un vec3
 *
 * v1, v2 : les deux vecteurs à ajouter
 *          ils ne sont pas modifiés
 */
vec3 vec3_add(vec3 v1, vec3 v2);

/* vec3_diff : renvoie v1-v2
 *            la valeur renvoyée est un vec3
 *
 * v1, v2 : les deux vecteurs à soustraire
 *          ils ne sont pas modifiés
 */
vec3 vec3_diff(vec3 v1, vec3 v2);

/* vec3_dot : renvoie le produit scalaire de v1 et v2
 *            la valeur renvoyée est un double
 *
 * v1, v2 : les deux vecteurs dont on calcule le ps
 *          ils ne sont pas modifiés
 */
double vec3_dot(vec3 v1, vec3 v2);

/* vec3_cross : renvoie le produit vectoriel de v1 et v2
 *            la valeur renvoyée est un vec3
 *
 * v1, v2 : les deux vecteurs dont on calcule le pv
 *          ils ne sont pas modifiés
 */
vec3 vec3_cross(vec3 v1, vec3 v2);

/* vec3_smul : renvoie le produit a * v
 *            correspondant à la multiplication scalaire
 *            la valeur renvoyée est un vec3
 *
 * a : double
 * v : vec3
 */
vec3 vec3_smul(double a, vec3 v);

/* vec3_norm : renvoie la norme du vecteur v
 *            la valeur renvoyée est un double
 *
 * v : vec3 dont on calcule la norme
 */
double vec3_norm(vec3 v);

/* vec3_normalize : renvoie un vecteur unitaire dirigé dans
 *                  la meme direction que v
 *
 *                  NOTE: si v est de norme nulle, renvoie un
 *                  vec3 arbitraire (1,0,0)
 * 
 * v : vec3
 */
vec3 vec3_normalize(vec3 v);

Question 2

Écrire le fichier vec3.c.

Corrigé
#include <math.h>
#include "vec3.h"

vec3 vec3_create(double x, double y, double z)
{
    vec3 v;
    v.x = x;
    v.y = y;
    v.z = z;
    return v;
}

vec3 vec3_add(vec3 v1, vec3 v2)
{
    return vec3_create(v1.x + v2.x,
                       v1.y + v2.y,
                       v1.z + v2.z);
}
vec3 vec3_diff(vec3 v1, vec3 v2)
{
    return vec3_create(v1.x - v2.x,
                       v1.y - v2.y,
                       v1.z - v2.z);
}

double vec3_dot(vec3 v1, vec3 v2)
{
    return v1.x * v2.x + v1.y * v2.y + v1.z * v2.z;
}

vec3 vec3_cross(vec3 v1, vec3 v2)
{
    return vec3_create(v1.y * v2.z - v1.z * v2.y,
                       v1.z * v2.x - v1.x * v2.z,
                       v1.x * v2.y - v1.y * v2.x);
}

vec3 vec3_smul(double a, vec3 v)
{
    return vec3_create(a * v.x, a * v.y, a * v.z);
}

double vec3_norm(vec3 v)
{
    return sqrt(vec3_dot(v, v));
}

vec3 vec3_normalize(vec3 v)
{
    double n = vec3_norm(v);

    if (fabs(n) < 1E-5) return vec3_create(1, 0, 0);

    return vec3_smul(1/n, v);
}

Question 3

Écrire le fichier vec3_test.c.

Corrigé

vec3_test.c :

#include <assert.h>
#include "vec3.h"

void vec3_create_test()
{
    vec3 v = vec3_create(1, 2, 3);
    assert(v.x == 1);
    assert(v.y == 2);
    assert(v.z == 3);
}

void vec3_add_test()
{
    vec3 v1 = vec3_create(5, 6, 7);
    vec3 v2 = vec3_create(1, 0, 2);
    vec3 v = vec3_add(v1, v2);

    assert(v.x == 6);
    assert(v.y == 6);
    assert(v.z == 9);
}

void vec3_diff_test()
{
    vec3 v1 = vec3_create(5, 6, 7);
    vec3 v2 = vec3_create(1, 0, 2);
    vec3 v = vec3_diff(v1, v2);

    assert(v.x == 4);
    assert(v.y == 6);
    assert(v.z == 5);
}

void vec3_dot_test()
{
    vec3 v1 = vec3_create(5, 6, 7);
    vec3 v2 = vec3_create(1, 0, 2);

    assert(vec3_dot(v1, v2) == 19);
}

void vec3_cross_test()
{
    vec3 v1 = vec3_create(1,0,0);
    vec3 v2 = vec3_create(0,1,0);
    vec3 v = vec3_cross(v1, v2);

    assert(v.x == 0 && v.y == 0 && v.z == 1);
}

void vec3_smul_test()
{
    vec3 v = vec3_create(1,2,3);
    vec3 sv = vec3_smul(2, v);
    assert(sv.x == 2 && sv.y == 4 && sv.z == 6);
}

void vec3_norm_test()
{
    assert(vec3_norm(vec3_create(3,4,0)) == 5);
}

void vec3_normalize_test()
{
    vec3 v = vec3_create(3,0,0);
    vec3 nv = vec3_normalize(v);
    assert(nv.x == 1 && nv.y == 0 && nv.z == 0);
}

void vec3_test()
{
    vec3_create_test();
    vec3_add_test();
    vec3_diff_test();
    vec3_dot_test();
    vec3_cross_test();
    vec3_smul_test();
    vec3_norm_test();
    vec3_normalize_test();
}

vec3_test.h :

void vec3_test();

et le fichier principal :

#include <stdlib.h>
#include <stdbool.h>
#include <math.h>
#include "vec3_test.h"

int main()
{
    vec3_test();   

    return 0;
}