Ce document s'inspire de "Ray Tracing: The Next Week" de Peter Shirley (2016). Il aborde des techniques avancées en rendu par lancer de rayons, notamment le flou de mouvement, les hiérarchies de volumes englobants (BVH), le mappage de textures, le bruit de Perlin, l'éclairage, les instances, et les volumes.
En raison des limitations de puissance de calcul, les images rendues ont une résolution réduite et sont fournies dans le dossier "The-Next-Week".
Chapitre 1 : Flou de Mouvement
Le flou de mouvement simule le flou observé dans le monde réel lorsque la caméra capture un mouvement sur une période d'exposition (l'ouverture de l'obturateur). Pour reproduire cet effet en lancer de rayons, nous échantillonnons plusieurs rayons par pixel sur une durée temporelle définie. Cela nécessite d'ajouter une information temporelle à la classe ray.
// Classe ray étendue pour inclure le temps
class ray {
public:
ray() {}
ray(const vec3& origin, const vec3& direction) : orig(origin), dir(direction) {}
// Constructeur avec temps
ray(const vec3& origin, const vec3& direction, float time) : orig(origin), dir(direction), tm(time) {}
vec3 origin() const { return orig; }
vec3 direction() const { return dir; }
// Calcul du point à un paramètre t
vec3 point_at_parameter(float t) const { return orig + t * dir; }
// Accesseur au temps
float time() const { return tm; }
vec3 orig; // Origine du rayon
vec3 dir; // Direction du rayon
float tm; // Temps d'émission du rayon
};
La classe camera est modifiée pour gérer un intervalle de temps (time0, time1) lors de la génération des rayons. L'ouverture de l'objectif (aperture) et la distance de mise au point (focus_dist) sont utilisées pour introduire un flou de profondeur de champ, combiné au flou de mouvement via l'échantillonnage temporel.
class camera {
public:
// Constructeur mis à jour pour inclure les temps
camera(vec3 lookfrom, vec3 lookat, vec3 vup, float vfov, float aspect,
float aperture, float focus_dist, float t0 = 0.0, float t1 = 0.0)
: origin(lookfrom), aperture_(aperture), focus_dist_(focus_dist), time0_(t0), time1_(t1) {
float theta = vfov * M_PI / 180;
float half_height = tan(theta / 2);
float half_width = aspect * half_height;
lens_radius_ = aperture / 2;
w_ = unit_vector(lookfrom - lookat);
u_ = unit_vector(cross(vup, w_));
v_ = cross(w_, u_);
lower_left_corner_ = origin - half_width * focus_dist_ * u_ - half_height * focus_dist_ * v_ - focus_dist_ * w_;
horizontal_ = 2 * half_width * focus_dist_ * u_;
vertical_ = 2 * half_height * focus_dist_ * v_;
}
// Génération d'un rayon aléatoire avec flou de profondeur et de mouvement
ray get_ray(float s, float t) const {
vec3 rd = lens_radius_ * random_in_unit_disk();
vec3 offset = u_ * rd.x() + v_ * rd.y();
// Génération d'un temps aléatoire dans l'intervalle [time0_, time1_]
float time = time0_ + drand48() * (time1_ - time0_);
return ray(origin + offset, lower_left_corner_ + s * horizontal_ + t * vertical_ - origin - offset, time);
}
private:
vec3 origin_;
vec3 lower_left_corner_;
vec3 horizontal_;
vec3 vertical_;
vec3 u_, v_, w_;
float lens_radius_;
float time0_, time1_; // Intervalle de temps pour le flou de mouvement
};
Pour gérer le mouvement des objets, une nouvelle classe moving_sphere est introduite. Elle stocke deux positions de centre (center0, center1) et deux instants (time0, time1) pour définir le mouvement sphérique sur cet intervalle de temps.
class moving_sphere : public hitable {
public:
moving_sphere() {}
// Constructeur pour une sphère en mouvement
moving_sphere(vec3 cen0, vec3 cen1, float t0, float t1, float r, material *m)
: center0_(cen0), center1_(cen1), time0_(t0), time1_(t1), radius_(r), mat_ptr_(m) {}
// Test d'intersection avec le rayon
virtual bool hit(const ray& r, float t_min, float t_max, hit_record& rec) const override;
// Calcul du centre de la sphère à un temps donné
vec3 center(float time) const {
return center0_ + ((time - time0_) / (time1_ - time0_)) * (center1_ - center0_);
}
vec3 center0_, center1_; // Centres aux temps t0 et t1
float time0_, time1_; // Intervalles de temps pour le mouvement
float radius_; // Rayon de la sphère
material *mat_ptr_; // Pointeur vers le matériau
};
// Implémentation de la fonction de hit pour la sphère en mouvement
bool moving_sphere::hit(const ray& r, float t_min, float t_max, hit_record& rec) const {
vec3 center_at_time = center(r.time()); // Utilise le temps du rayon pour trouver le centre
vec3 oc = r.origin() - center_at_time;
float a = dot(r.direction(), r.direction());
float b = 2.0 * dot(oc, r.direction());
float c = dot(oc, oc) - radius_ * radius_;
float discriminant = b * b - a * c;
if (discriminant > 0) {
float temp = (-b - sqrt(discriminant)) / a;
if (temp < t_max && temp > t_min) {
rec.t = temp;
rec.p = r.point_at_parameter(rec.t);
rec.normal = (rec.p - center_at_time) / radius_;
rec.mat_ptr = mat_ptr_;
return true;
}
temp = (-b + sqrt(discriminant)) / a;
if (temp < t_max && temp > t_min) {
rec.t = temp;
rec.p = r.point_at_parameter(rec.t);
rec.normal = (rec.p - center_at_time) / radius_;
rec.mat_ptr = mat_ptr_;
return true;
}
}
return false;
}
La fonction random_scene() est mise à jour pour inclure des moving_sphere, créant ainsi un mouvement subtil dans la scène de démonstration du flou de mouvement.
Chapitre 2 : Hiérarchies de Volumes Englobants (BVH)
Pour optimiser le lancer de rayons, particulièrement dans les scènes complexes, les hiérarchies de volumes englobants (BVH) sont introduites. L'idée principale est de regrouper les objets en volumes englobants plus petits, puis de construire une structure arborescente. Si un rayon ne touche pas le volume englobant d'un groupe d'objets, il ne peut pas toucher les objets à l'intérieur, ce qui permet d'élaguer de nombreuses intersections potentielles.
La classe aabb (Axis-Aligned Bounding Box) est définie pour représenter des boîtes englobantes alignées sur les axes. Elle inclut une fonction hit pour tester l'intersection d'un rayon avec la boîte.
// Classe pour les boîtes englobantes alignées sur les axes
class aabb {
public:
aabb() {}
aabb(const vec3& a, const vec3& b) {
// Assure que _min contient les coordonnées minimales et _max les maximales
_min = vec3(fmin(a.x(), b.x()), fmin(a.y(), b.y()), fmin(a.z(), b.z()));
_max = vec3(fmax(a.x(), b.x()), fmax(a.y(), b.y()), fmax(a.z(), b.z()));
}
vec3 min() const { return _min; }
vec3 max() const { return _max; }
// Test d'intersection d'un rayon avec la boîte englobante
bool hit(const ray& r, float t_min, float t_max) const {
// Itération sur les 3 axes (x, y, z)
for (int a = 0; a < 3; a++) {
float invD = 1.0f / r.direction()[a]; // Inverse de la composante de direction de l'axe
float t0 = (_min[a] - r.origin()[a]) * invD; // Temps d'intersection avec le plan proche
float t1 = (_max[a] - r.origin()[a]) * invD; // Temps d'intersection avec le plan lointain
// Assure que t0 est le temps le plus proche et t1 le plus éloigné
if (invD < 0.0f)
std::swap(t0, t1);
// Met à jour le temps minimum et maximum de l'intersection globale
t_min = fmax(t0, t_min);
t_max = fmin(t1, t_max);
// Si le temps maximum devient inférieur au temps minimum, il n'y a pas d'intersection
if (t_max <= t_min)
return false;
}
return true; // Intersection trouvée
}
vec3 _min; // Coin inférieur-gauche-arrière
vec3 _max; // Coin supérieur-droit-avant
};
La classe hitable est étendue avec une fonction virtuelle bounding_box. Les classes dérivées (comme sphere) doivent implémenter cette fonction pour définir leur boîte englobante.
La classe bvh_node représente un nœud dans l'arbre BVH. Elle contient des pointeurs vers ses enfants gauche et droit (left, right) et sa propre boîte englobante (box). La fonction hit d'un nœud BVH teste d'abord sa boîte englobante. Si le rayon touche la boîte, elle teste récursivement les enfants gauche et droit.
// Nœud de l'arbre BVH
class bvh_node : public hitable {
public:
bvh_node() {}
// Constructeur pour un nœud BVH
bvh_node(hitable **l, int n, float time0, float time1);
// Test d'intersection avec le rayon
virtual bool hit(const ray& r, float tmin, float tmax, hit_record& rec) const override;
// Calcul de la boîte englobante
virtual bool bounding_box(float t0, float t1, aabb& box) const override {
box = box_;
return has_box_;
}
hitable *left; // Enfant gauche
hitable *right; // Enfant droit
aabb box_; // Boîte englobante du nœud
bool has_box_; // Indicateur si la boîte est valide
};
// Constructeur du nœud BVH (implémentation simplifiée ici)
bvh_node::bvh_node(hitable **l, int n, float time0, float time1) : has_box_(false) {
int axis = int(3 * drand48()); // Choix aléatoire de l'axe de division
auto box_compare = [axis](const hitable *a, const hitable *b) {
aabb box_a, box_b;
if (!a->bounding_box(0.0, 0.0, box_a) || !b->bounding_box(0.0, 0.0, box_b))
std::cerr << "Pas de boîte englobante dans le constructeur de bvh_node\n";
return box_a.min()[axis] < box_b.min()[axis] ? -1 : 1;
};
// Utilisation de qsort pour trier les objets selon l'axe choisi
// Une implémentation plus robuste utiliserait std::sort ou un algorithme de partitionnement
qsort(l, n, sizeof(hitable*), box_compare);
if (n == 1) {
left = right = l[0];
} else {
left = new bvh_node(l, n / 2, time0, time1);
right = new bvh_node(l + n / 2, n - n / 2, time0, time1);
}
aabb box_left, box_right;
left->bounding_box(time0, time1, box_left);
right->bounding_box(time0, time1, box_right);
box_ = aabb(box_left.min(), box_right.max());
has_box_ = true; // Assurez-vous que has_box_ est défini si vous avez des objets
}
// Implémentation de la fonction hit pour le nœud BVH
bool bvh_node::hit(const ray& r, float tmin, float tmax, hit_record& rec) const {
if (has_box_ && !box_.hit(r, tmin, tmax)) {
return false; // Si le rayon ne touche pas la boîte englobante, on arrête
}
// Test des enfants gauche et droit
hit_record temp_rec;
bool hit_anything = false;
float closest_so_far = tmax;
// Tenter de toucher l'enfant gauche
if (left->hit(r, tmin, closest_so_far, temp_rec)) {
hit_anything = true;
closest_so_far = temp_rec.t;
rec = temp_rec;
}
// Tenter de toucher l'enfant droit, en utilisant le temps de l'intersection la plus proche trouvée jusqu'à présent
if (right->hit(r, tmin, closest_so_far, temp_rec)) {
hit_anything = true;
closest_so_far = temp_rec.t;
rec = temp_rec;
}
return hit_anything;
}
Chaptire 3 : Textures Solides
Au lieu d'utiliser des couleurs unies pour les matériaux, nous introduisons un système de textures. La classe abstraite texture définit une interface pour obtenir une couleur en fonction des coordonnées UV ou de la position 3D. constant_texture est une implémentation simple qui renvoie une couleur constante.
// Classe abstraite pour les textures
class texture {
public:
virtual vec3 value(float u, float v, const vec3& p) const = 0;
};
// Texture à couleur constante
class constant_texture : public texture {
public:
constant_texture() {}
constant_texture(vec3 c) : color_(c) {}
// Retourne la couleur constante définie
virtual vec3 value(float u, float v, const vec3& p) const override {
return color_;
}
vec3 color_;
};
La classe lambertian (matériau diffus) est mise à jour pour accepter un pointeur vers un objet texture au lieu d'une couleur vec3 directe. Cela permet d'associer des textures complexes aux matériaux diffus.
checker_texture implémente un motif de damier en alternant entre deux textures de base en fonction de la position de l'objet.
// Texture en damier
class checker_texture : public texture {
public:
checker_texture() {}
checker_texture(texture *t0, texture *t1) : even_(t0), odd_(t1) {}
// Retourne la couleur basée sur la position 3D pour créer un damier
virtual vec3 value(float u, float v, const vec3& p) const override {
// Calcul basé sur les coordonnées de p pour créer le motif
float sines = sin(10 * p.x()) * sin(10 * p.y()) * sin(10 * p.z());
if (sines < 0)
return odd_->value(u, v, p);
else
return even_->value(u, v, p);
}
texture *even_; // Texture pour les carrés pairs
texture *odd_; // Texture pour les carrés impairs
};
Chapitre 4 : Bruit de Perlin
Le bruit de Perlin est une technique de génération de nombres pseudo-aléatoires qui produit des gradients naturels et fluides. Il est utilisé ici pour créer des textures procédurales réalistes comme des surfaces rocheuses ou des nuages.
La classe perlin implémente l'algorithme du bruit de Perlin. Elle utilise des permutations et des gradients aléatoires pour générer des valeurs de bruit continues et cohérentes dans l'espace 3D.
// Classe pour le bruit de Perlin
class perlin {
public:
float noise(const vec3& p) const {
float u = p.x() - floor(p.x());
float v = p.y() - floor(p.y());
float w = p.z() - floor(p.z());
// Lissage Hermite cubique pour une interpolation plus douce
u = u * u * (3 - 2 * u);
v = v * v * (3 - 2 * v);
w = w * w * (3 - 2 * w);
int i = floor(p.x());
int j = floor(p.y());
int k = floor(p.z());
// Interpolation entre les 8 coins du cube unité environnant
vec3 c[2][2][2];
for (int di = 0; di < 2; di++) {
for (int dj = 0; dj < 2; dj++) {
for (int dk = 0; dk < 2; dk++) {
// Utilisation des tables de permutation pour obtenir des gradients pseudo-aléatoires
int pi = perm_x_[(i + di) & 255];
int pj = perm_y_[(j + dj) & 255];
int pk = perm_z_[(k + dk) & 255];
// Vecteur de poids pour le produit scalaire
vec3 weight_v(u - di, v - dj, w - dk);
// Produit scalaire entre le gradient et le vecteur de poids
c[di][dj][dk] = ranfloat_[pi ^ pj ^ pk] * weight_v;
}
}
}
return perlin_interp(c, u, v, w);
}
// Fonction pour générer du bruit de Perlin turbulent (fractal brownien)
float turb(const vec3& p, int depth = 7) const {
float accum = 0;
vec3 temp_p = p;
float weight = 1.0;
for (int i = 0; i < depth; i++) {
accum += weight * noise(temp_p);
weight *= 0.5; // Atténuation du poids à chaque octave
temp_p *= 2; // Mise à l'échelle de la position pour simuler des détails plus fins
}
return fabs(accum); // Retourne la valeur absolue pour obtenir une texture positive
}
private:
// Interpolation Hermite cubique pour le bruit de Perlin
inline float perlin_interp(vec3 c[2][2][2], float u, float v, float w) const {
float uu = u * u * (3 - 2 * u);
float vv = v * v * (3 - 2 * v);
float ww = w * w * (3 - 2 * w);
float accum = 0;
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 2; ++j) {
for (int k = 0; k < 2; ++k) {
vec3 weight_v(u - i, v - j, w - k);
accum += (i * uu + (1 - i) * (1 - uu)) *
(j * vv + (1 - j) * (1 - vv)) *
(k * ww + (1 - k) * (1 - ww)) * dot(c[i][j][k], weight_v);
}
}
}
return accum;
}
// Génération des tables de permutations et des valeurs aléatoires
static float *ranfloat_;
static int *perm_x_;
static int *perm_y_;
static int *perm_z_;
// Fonctions statiques pour initialiser les tables
static float* perlin_generate_() {
float* p = new float[256];
for (int i = 0; i < 256; i++) {
p[i] = 2.0 * drand48() - 1.0; // Valeurs entre -1 et 1
}
return p;
}
static void permute_(int* p, int n) {
for (int i = n - 1; i > 0; i--) {
int target = int(drand48() * (i + 1));
std::swap(p[i], p[target]);
}
}
static int* perlin_generate_perm_() {
int* p = new int[256];
for (int i = 0; i < 256; i++) {
p[i] = i;
}
permute_(p, 256);
return p;
}
};
// Initialisation statique des tables globales pour le bruit de Perlin
float *perlin::ranfloat_ = perlin::perlin_generate_();
int *perlin::perm_x_ = perlin::perlin_generate_perm_();
int *perlin::perm_y_ = perlin::perlin_generate_perm_();
int *perlin::perm_z_ = perlin::perlin_generate_perm_();
noise_texture utilise le bruit de Perlin pour créer des variations de couleur procédurales. L'échelle et la perturbation (turb) permettent de contrôler l'apparence de la texture.
Chapitre 5 : Mappage de Textures d'Image
Cette section explique comment mapper des images 2D sur des surfaces 3D. Les coordonnées UV (normalisées entre 0 et 1) sont utilisées pour faire correspondre les pixels de l'image aux points sur la surface de l'objet.
Pour les sphères, une fonction get_sphere_uv est fournie pour convertir les coordonnées cartésiennes 3D d'un point d'intersection en coordonnées UV sphériques.
// Fonction utilitaire pour obtenir les coordonnées UV d'une sphère
void get_sphere_uv(const vec3& p, float& u, float& v) {
// p est un point sur la sphère unité
float phi = atan2(p.z(), p.x()); // Angle autour de l'axe Y
float theta = asin(p.y()); // Angle par rapport à l'axe XZ
// Normalisation des angles pour obtenir les coordonnées UV [0, 1]
// phi varie de -PI à PI, donc on ajoute PI et on divise par 2*PI
u = 1 - (phi + M_PI) / (2 * M_PI);
// theta varie de -PI/2 à PI/2, donc on ajoute PI/2 et on divise par PI
v = (theta + M_PI / 2) / M_PI;
}
La classe image_texture charge les données d'une image (en utilisant une bibliothèque comme stb_image) et renvoie la couleur d'un pixel donné en fonction des coordonnées UV interpolées.
// Texture basée sur une image
class image_texture : public texture {
public:
image_texture() {}
// Constructeur : nécessite les données de pixels, la largeur (nx) et la hauteur (ny)
image_texture(unsigned char *pixels, int A, int B) : data_(pixels), nx_(A), ny_(B) {}
~image_texture() { delete[] data_; } // Nettoyage de la mémoire
// Méthode pour obtenir la couleur à une coordonnée UV donnée
virtual vec3 value(float u, float v, const vec3& p) const override;
unsigned char *data_; // Données des pixels de l'image (format RGB)
int nx_, ny_; // Dimensions de l'image
};
// Implémentation de la méthode value pour image_texture
vec3 image_texture::value(float u, float v, const vec3& p) const {
// Assurer que les coordonnées UV sont dans l'intervalle [0, 1]
if (u < 0.0) u = 0.0;
if (v < 0.0) v = 0.0;
if (u > 1.0) u = 1.0;
if (v > 1.0) v = 1.0;
// Calcul des indices de pixel correspondant aux coordonnées UV
// L'axe Y de l'image est souvent inversé par rapport à l'UV standard
int i = int(u * nx_);
int j = int((1 - v) * ny_); // Inversion de v pour l'indexation de l'image
// Clamp les indices pour rester dans les limites de l'image
if (i < 0) i = 0;
if (j < 0) j = 0;
if (i >= nx_) i = nx_ - 1;
if (j >= ny_) j = ny_ - 1;
// Calcul de l'offset pour accéder aux données RGB du pixel
// Chaque pixel a 3 composantes (R, G, B)
float r_scale = 1.0f / 255.0f;
int offset = 3 * (i + nx_ * j); // Position du premier octet R pour ce pixel
// Récupération des valeurs RGB et conversion en vec3
float r = data_[offset] * r_scale;
float g = data_[offset + 1] * r_scale;
float b = data_[offset + 2] * r_scale;
return vec3(r, g, b);
}
Chapitre 6 : Rectangles et Lumières
Ce chapitre introduit des objets rectangulaires planaires et la capacité de créer des matériaux auto-émissifs.
diffuse_light est un nouveau type de matériau qui émet de la lumière. Il utilise une texture pour définir la couleur et l'intensité de l'émission.
// Matériau diffus émetteur de lumière
class diffuse_light : public material {
public:
diffuse_light(texture *emit_tex) : emit_(emit_tex) {}
// Un matériau émetteur ne diffuse pas la lumière incidente, il la génère
virtual bool scatter(const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered) const override {
return false; // Ne diffuse pas la lumière
}
// Retourne la couleur émise par la texture
virtual vec3 emitted(float u, float v, const vec3& p) const override {
return emit_->value(u, v, p);
}
texture *emit_; // Texture définissant la lumière émise
};
Les classes xy_rect, xz_rect, et yz_rect définissent des rectangles dans les plans coordonnés correspondants. Elles sont utilisées pour construire des objets plus complexes comme des murs ou des sources de lumière planes.
// Rectangle dans le plan XY
class xy_rect : public hitable {
public:
xy_rect() {}
xy_rect(float _x0, float _x1, float _y0, float _y1, float _kz, material *mat)
: x0_(_x0), x1_(_x1), y0_(_y0), y1_(_y1), kz_(_kz), mp_(mat) {}
// Test d'intersection avec le rayon
virtual bool hit(const ray& r, float t_min, float t_max, hit_record& rec) const override;
// Calcul de la boîte englobante
virtual bool bounding_box(float t0, float t1, aabb& box) const override {
box = aabb(vec3(x0_, y0_, kz_ - 0.0001), vec3(x1_, y1_, kz_ + 0.0001));
return true;
}
material *mp_; // Matériau du rectangle
float x0_, x1_, y0_, y1_, kz_; // Définition des limites du rectangle et de la coordonnée k
};
// Implémentation de hit pour xy_rect
bool xy_rect::hit(const ray& r, float t_min, float t_max, hit_record& rec) const {
// Calcul du temps d'intersection avec le plan z=kz_
float t = (kz_ - r.origin().z()) / r.direction().z();
if (t < t_min || t > t_max) // Vérifie si l'intersection est dans l'intervalle de temps valide
return false;
// Calcul des coordonnées x et y du point d'intersection sur le plan
float x = r.origin().x() + t * r.direction().x();
float y = r.origin().y() + t * r.direction().y();
// Vérifie si le point d'intersection se trouve à l'intérieur des limites du rectangle
if (x < x0_ || x > x1_ || y < y0_ || y > y1_)
return false;
// Calcul des coordonnées UV pour le mappage de texture
rec.u = (x - x0_) / (x1_ - x0_);
rec.v = (y - y0_) / (y1_ - y0_);
rec.t = t;
rec.mat_ptr = mp_;
rec.p = r.point_at_parameter(t);
rec.normal = vec3(0, 0, 1); // Normale pour un rectangle sur le plan XY
return true;
}
Ces rectangles sont ensuite utilisés pour construire des scènes comme la "Cornell Box", une scène de test classique pour les simulateurs de lumière.
Chapitre 7 : Instances
L'instanciation permet de réutiliser des objets complexes en les déplaçant et en les faisant pivoter sans avoir à les redéfinir entièrement. Cela est particulièrement utile pour créer des scènes avec des objets répétés ou pour construire des modèles complexes à partir de primitives plus simples.
La classe box est implémentée en utilisant six rect pour former un cube. Elle gère son propre bounding_box.
// Classe pour une boîte définie par deux coins opposés
class box : public hitable {
public:
box() {}
box(const vec3& p0, const vec3& p1, material *ptr); // Constructeur
// Test d'intersection avec le rayon
virtual bool hit(const ray& r, float t0, float t1, hit_record& rec) const override;
// Calcul de la boîte englobante
virtual bool bounding_box(float t0, float t1, aabb& box) const override {
box = aabb(pmin_, pmax_); // La boîte englobante est la boîte elle-même
return true;
}
vec3 pmin_, pmax_; // Coins opposés définissant la boîte
hitable *list_ptr_; // Liste des 6 rectangles formant la boîte
};
// Constructeur de la boîte, crée les 6 rectangles faces
box::box(const vec3& p0, const vec3& p1, material *ptr) {
pmin_ = p0;
pmax_ = p1;
hitable **list = new hitable*[6];
// Création des 6 faces rectangulaires, avec inversion des normales pour les faces internes
list[0] = new xy_rect(p0.x(), p1.x(), p0.y(), p1.y(), p1.z(), ptr); // Face avant
list[1] = new flip_normals(new xy_rect(p0.x(), p1.x(), p0.y(), p1.y(), p0.z(), ptr)); // Face arrière
list[2] = new xz_rect(p0.x(), p1.x(), p0.z(), p1.z(), p1.y(), ptr); // Face dessus
list[3] = new flip_normals(new xz_rect(p0.x(), p1.x(), p0.z(), p1.z(), p0.y(), ptr)); // Face dessous
list[4] = new yz_rect(p0.y(), p1.y(), p0.z(), p1.z(), p1.x(), ptr); // Face droite
list[5] = new flip_normals(new yz_rect(p0.y(), p1.y(), p0.z(), p1.z(), p0.x(), ptr)); // Face gauche
list_ptr_ = new hitable_list(list, 6); // Crée une liste d'objets hitable
}
// La fonction hit de la boîte délègue simplement à la liste de ses faces
bool box::hit(const ray& r, float t0, float t1, hit_record& rec) const {
return list_ptr_->hit(r, t0, t1, rec);
}
translate et rotate_y sont des classes d'instance qui prennent un objet hitable existant et le transforment (déplacement ou rotation). Ces transformations sont appliquées en modifiant les coordonnées du rayon avant le test d'intersection et en ajustant le résultat de l'intersection (position et normale).
// Classe pour translater un objet hitable
class translate : public hitable {
public:
translate(hitable *p, const vec3& displacement) : ptr_(p), offset_(displacement) {}
// Test d'intersection, en déplaçant le rayon inversement
virtual bool hit(const ray& r, float t_min, float t_max, hit_record& rec) const override;
// Calcul de la boîte englobante, en déplaçant la boîte originale
virtual bool bounding_box(float t0, float t1, aabb& box) const override;
hitable *ptr_; // Objet à translater
vec3 offset_; // Vecteur de translation
};
// Implémentation de hit pour translate
bool translate::hit(const ray& r, float t_min, float t_max, hit_record& rec) const {
// Crée un rayon "déplacé" inversement pour tester l'objet original
ray moved_r(r.origin() - offset_, r.direction(), r.time());
if (ptr_->hit(moved_r, t_min, t_max, rec)) {
rec.p += offset_; // Ajoute le décalage à la position d'intersection
return true;
}
return false;
}
// Implémentation de bounding_box pour translate
bool translate::bounding_box(float t0, float t1, aabb& box) const {
if (ptr_->bounding_box(t0, t1, box)) {
// Déplace les limites de la boîte englobante originale
box = aabb(box.min() + offset_, box.max() + offset_);
return true;
}
return false;
}
// Classe pour faire pivoter un objet hitable autour de l'axe Y
class rotate_y : public hitable {
public:
rotate_y(hitable *p, float angle); // Constructeur
// Test d'intersection, en appliquant la rotation inverse au rayon
virtual bool hit(const ray& r, float t_min, float t_max, hit_record& rec) const override;
// Calcul de la boîte englobante, en tenant compte de la rotation
virtual bool bounding_box(float t0, float t1, aabb& box) const override {
box = bbox_;
return has_box_;
}
hitable *ptr_; // Objet à faire pivoter
float sin_theta_, cos_theta_; // Cosinus et sinus de l'angle de rotation
bool has_box_; // Indicateur si la boîte est valide
aabb bbox_; // Boîte englobante après rotation
};
// Constructeur de rotate_y
rotate_y::rotate_y(hitable *p, float angle) : ptr_(p) {
float radians = (M_PI / 180.) * angle;
sin_theta_ = sin(radians);
cos_theta_ = cos(radians);
has_box_ = ptr_->bounding_box(0.0, 1.0, bbox_); // Obtient la boîte englobante initiale
// Recalcule la boîte englobante après rotation pour la couvrir complètement
vec3 min(FLT_MAX, FLT_MAX, FLT_MAX);
vec3 max(-FLT_MAX, -FLT_MAX, -FLT_MAX);
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 2; j++) {
for (int k = 0; k < 2; k++) {
// Coordonnées des coins de la boîte originale
float x = i * bbox_.max().x() + (1 - i) * bbox_.min().x();
float y = j * bbox_.max().y() + (1 - j) * bbox_.min().y();
float z = k * bbox_.max().z() + (1 - k) * bbox_.min().z();
// Applique la rotation autour de Y
float newx = cos_theta_ * x + sin_theta_ * z;
float newz = -sin_theta_ * x + cos_theta_ * z;
vec3 tester(newx, y, newz); // Point après rotation
// Met à jour les limites min/max de la nouvelle boîte englobante
for (int c = 0; c < 3; c++) {
if (tester[c] > max[c]) max[c] = tester[c];
if (tester[c] < min[c]) min[c] = tester[c];
}
}
}
}
bbox_ = aabb(min, max); // Définit la nouvelle boîte englobante ajustée
}
// Implémentation de hit pour rotate_y
bool rotate_y::hit(const ray& r, float t_min, float t_max, hit_record& rec) const {
vec3 origin = r.origin();
vec3 direction = r.direction();
// Applique la rotation inverse à l'origine et à la direction du rayon
origin[0] = cos_theta_ * r.origin()[0] - sin_theta_ * r.origin()[2];
origin[2] = sin_theta_ * r.origin()[0] + cos_theta_ * r.origin()[2];
direction[0] = cos_theta_ * r.direction()[0] - sin_theta_ * r.direction()[2];
direction[2] = sin_theta_ * r.direction()[0] + cos_theta_ * r.direction()[2];
ray rotated_r(origin, direction, r.time()); // Crée le rayon tourné
// Teste l'intersection avec l'objet original tourné
if (ptr_->hit(rotated_r, t_min, t_max, rec)) {
// Rétablit l'orientation originale du point d'intersection et de la normale
vec3 p = rec.p;
vec3 normal = rec.normal;
// Rotation inverse pour le point et la normale
p[0] = cos_theta_ * rec.p[0] + sin_theta_ * rec.p[2];
p[2] = -sin_theta_ * rec.p[0] + cos_theta_ * rec.p[2];
normal[0] = cos_theta_ * rec.normal[0] + sin_theta_ * rec.normal[2];
normal[2] = -sin_theta_ * rec.normal[0] + cos_theta_ * rec.normal[2];
rec.p = p;
rec.normal = normal;
return true;
}
return false;
}
Chapitre 8 : Volumes
Le rendu de volumes introduit la simulation de brouillard, de fumée ou d'autres effets atmosphériques. Les rayons peuvent interagir avec la matière à l'intérieur du volume, subissant des diffusions et des absorptions.
constant_medium représente un volume avec une densité constante. Il utilise une fonction de phase (ici, isotropic pour une diffusion uniforme) pour déterminer comment la lumière est diffusée à l'intérieur du volume. Le calcul de l'intersection avec un volume implique de trouver les points d'entrée et de sortie, puis de simuler des événements de diffusion à l'intérieur basés sur la densité.
// Matériau isotrope (diffusion uniforme dans toutes les directions)
class isotropic : public material {
public:
isotropic(texture *a) : albedo_(a) {}
// La diffusion est aléatoire dans toutes les directions
virtual bool scatter(const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered) const override {
// Le rayon diffusé part dans une direction aléatoire
scattered = ray(rec.p, random_in_unit_sphere(), r_in.time());
// L'atténuation est déterminée par la texture de l'albédo
attenuation = albedo_->value(rec.u, rec.v, rec.p);
return true;
}
texture *albedo_; // Texture définissant la couleur de la diffusion
};
// Volume à densité constante
class constant_medium : public hitable {
public:
constant_medium(hitable *b, float d, texture *a) : boundary_(b), density_(d) {
phase_function_ = new isotropic(a); // Utilise un matériau isotrope pour la diffusion
}
~constant_medium() { delete phase_function_; } // Nettoyage
// Test d'intersection, vérifie les intersections avec la frontière et simule la diffusion interne
virtual bool hit(const ray& r, float t_min, float t_max, hit_record& rec) const override;
// La boîte englobante est celle de la frontière du volume
virtual bool bounding_box(float t0, float t1, aabb& box) const override {
return boundary_->bounding_box(t0, t1, box);
}
hitable *boundary_; // La frontière délimitant le volume
float density_; // Densité du milieu
material *phase_function_; // Matériau décrivant la diffusion interne
};
// Implémentation de hit pour constant_medium
bool constant_medium::hit(const ray& r, float t_min, float t_max, hit_record& rec) const {
hit_record rec1, rec2;
// Trouve la première intersection avec la frontière
if (!boundary_->hit(r, t_min, t_max, rec1))
return false;
// Trouve la deuxième intersection avec la frontière (si elle existe)
// On cherche après le premier point d'impact, avec une petite marge pour éviter les auto-intersections
if (!boundary_->hit(r, rec1.t + 0.0001, FLT_MAX, rec2))
return false;
// Ajuste les temps d'intersection pour rester dans l'intervalle [t_min, t_max]
if (rec1.t < t_min) rec1.t = t_min;
if (rec2.t > t_max) rec2.t = t_max;
// Si les points d'entrée et de sortie sont trop proches ou inversés, pas d'intersection significative
if (rec1.t >= rec2.t)
return false;
// Si le premier point d'impact est avant le début du trajet de la caméra, le considérer comme le début
if (rec1.t < 0) rec1.t = 0;
// Distance parcourue à l'intérieur du volume
float distance_inside_boundary = (rec2.t - rec1.t) * r.direction().length();
// Distance aléatoire parcourue avant une première diffusion (basée sur la densité)
// Le terme log(drand48()) génère un nombre aléatoire dont la distribution est appropriée
float hit_distance = -(1 / density_) * log(drand48());
// Si la distance aléatoire est plus courte que la distance à travers le volume, une diffusion se produit
if (hit_distance < distance_inside_boundary) {
// Le point d'intersection est calculé sur le trajet du rayon
rec.t = rec1.t + hit_distance / r.direction().length();
rec.p = r.point_at_parameter(rec.t);
// La normale est arbitraire ici car c'est une diffusion interne
// Dans des simulations plus complexes, une normale pourrait être calculée
rec.normal = vec3(1, 0, 0); // Normale arbitraire (peut être remplacée)
rec.mat_ptr = phase_function_; // Le matériau est la fonction de phase du volume
return true;
}
return false; // Aucune diffusion ne s'est produite dans l'intervalle considéré
}
Chapitre 9 : Scène Testant Toutes les Nouvelles Fonctionnalités
Le dernier chapitre rassemble toutes les techniques introduites pour recréer une scène complexe et visuellement impressionnante, similaire à celle de la couverture du livre. Cette scène intègre le flou de mouvement, les BVH, les textures d'images, le bruit de Perlin, l'éclairage, les instances et les volumes.
La fonction final() construit cette scène en combinant divers éléments, tels que des boîtes imbriquées, des sphères avec différents matériaux, des sources de lumière rectangulaires et des volumes de brouillard coloré.
// Fonction pour créer la scène finale complexe
hitable *final_scene() {
int nb_boxes = 10; // Nombre de boîtes par dimension pour le sol
hitable **list = new hitable*[3000]; // Allocation d'espace pour les objets
material *white_mat = new lambertian(new constant_texture(vec3(0.73, 0.73, 0.73))); // Matériau blanc
material *ground_mat = new lambertian(new constant_texture(vec3(0.48, 0.83, 0.53))); // Matériau vert pour le sol
int index = 0; // Index courant pour la liste d'objets
// Création d'un sol composé de nombreuses petites boîtes
for (int i = 0; i < nb_boxes; i++) {
for (int j = 0; j < nb_boxes; j++) {
float w = 100.0; // Taille de chaque boîte
float x0 = i * w;
float z0 = j * w;
float y0 = 0;
// Hauteur aléatoire pour chaque boîte du sol
float y1 = 100.0 * (drand48() + 0.01);
float x1 = x0 + w;
float z1 = z0 + w;
list[index++] = new box(vec3(x0, y0, z0), vec3(x1, y1, z1), ground_mat);
}
}
// Ajout d'une source de lumière rectangulaire
material *light_mat = new diffuse_light(new constant_texture(vec3(7, 7, 7)));
list[index++] = new xz_rect(123, 423, 147, 412, 554, light_mat); // Position et taille de la lumière
// Ajout d'une sphère en mouvement
vec3 center_moving_sphere(400, 400, 200);
list[index++] = new moving_sphere(center_moving_sphere, center_moving_sphere + vec3(30, 0, 0),
0.0, 1.0, 50, new lambertian(new constant_texture(vec3(0.7, 0.3, 0.1))));
// Ajout d'une sphère diélectrique (verre)
list[index++] = new sphere(vec3(260, 150, 45), 50, new dielectric(1.5));
// Ajout d'une sphère métallique
list[index++] = new sphere(vec3(0, 150, 145), 50, new metal(vec3(0.8, 0.8, 0.9), 10.0));
// Ajout d'une sphère contenant un volume de brouillard
hitable *boundary = new sphere(vec3(360, 150, 145), 70, new dielectric(1.5)); // Sphère délimitant le volume
list[index++] = new constant_medium(boundary, 0.2, new constant_texture(vec3(0.2, 0.4, 0.9))); // Volume bleu
// Ajout d'un grand volume de brouillard très léger entourant la scène
boundary = new sphere(vec3(0, 0, 0), 5000, new dielectric(1.5)); // Grande sphère englobante
list[index++] = new constant_medium(boundary, 0.0001, new constant_texture(vec3(1.0, 1.0, 1.0))); // Brouillard blanc très léger
// Ajout d'une sphère avec une texture de bruit de Perlin
texture *perlin_tex = new noise_texture(0.1);
list[index++] = new sphere(vec3(220, 280, 300), 80, new lambertian(perlin_tex));
// Ajout de nombreuses petites sphères blanches pour remplir l'espace
int ns = 1000; // Nombre de petites sphères
for (int j = 0; j < ns; j++) {
list[index++] = new sphere(vec3(165 * drand48() - 100, 165 * drand48() + 270, 165 * drand48() + 395), 10, white_mat);
}
std::cout << "Nombre total d'objets dans la scène : " << index << std::endl; // Affichage du nombre total d'objets
return new hitable_list(list, index); // Retourne la liste complète des objets
}