Nous dessinons une explosion de dessin animé pour 180 lignes de C ++ nu

Il y a une semaine, j'ai publié un autre chapitre de mon cours d' informatique graphique ; Aujourd'hui, nous revenons au ray tracing, mais cette fois nous irons un peu plus loin que le rendu de sphères triviales. Je n'ai pas besoin de photoréalisme; à des fins caricaturales, une telle explosion , il me semble, va se produire.

Comme toujours, nous n'avons qu'un compilateur nu à notre disposition, aucune bibliothèque tierce ne peut être utilisée. Je ne veux pas m'embêter avec les gestionnaires de fenêtres, le traitement souris / clavier, etc. Le résultat de notre programme sera une simple image enregistrée sur le disque. Je ne poursuis pas du tout la vitesse / l'optimisation, mon objectif est de montrer les principes de base.

Au total, comment dessiner une telle image en 180 lignes de code dans de telles conditions?



Permettez-moi même d'insérer un gif animé (six mètres):



Et maintenant, nous allons diviser l'ensemble de la tâche en plusieurs étapes:

Première étape: lire l'article précédent


Oui, exactement. La toute première chose à faire est de lire le chapitre précédent , qui parle des bases du lancer de rayons. Il est très court, en principe, toutes les réflexions-réfractions ne peuvent pas être lues, mais au moins jusqu'à un éclairage diffus je recommande de le lire. Le code est assez simple, les gens l'exécutent même sur des microcontrôleurs:



Deuxième étape: dessiner une sphère


Dessinons une sphère sans se soucier des matériaux ou de l'éclairage. Par souci de simplicité, cette sphère vivra au centre des coordonnées. A propos de cette photo que je souhaite obtenir:



Voir le code ici , mais permettez-moi de vous donner le principal directement dans le texte de l'article:

#define _USE_MATH_DEFINES #include <cmath> #include <algorithm> #include <limits> #include <iostream> #include <fstream> #include <vector> #include "geometry.h" const float sphere_radius = 1.5; float signed_distance(const Vec3f &p) { return p.norm() - sphere_radius; } bool sphere_trace(const Vec3f &orig, const Vec3f &dir, Vec3f &pos) { pos = orig; for (size_t i=0; i<128; i++) { float d = signed_distance(pos); if (d < 0) return true; pos = pos + dir*std::max(d*0.1f, .01f); } return false; } int main() { const int width = 640; const int height = 480; const float fov = M_PI/3.; std::vector<Vec3f> framebuffer(width*height); #pragma omp parallel for for (size_t j = 0; j<height; j++) { // actual rendering loop for (size_t i = 0; i<width; i++) { float dir_x = (i + 0.5) - width/2.; float dir_y = -(j + 0.5) + height/2.; // this flips the image at the same time float dir_z = -height/(2.*tan(fov/2.)); Vec3f hit; if (sphere_trace(Vec3f(0, 0, 3), Vec3f(dir_x, dir_y, dir_z).normalize(), hit)) { // the camera is placed to (0,0,3) and it looks along the -z axis framebuffer[i+j*width] = Vec3f(1, 1, 1); } else { framebuffer[i+j*width] = Vec3f(0.2, 0.7, 0.8); // background color } } } std::ofstream ofs("./out.ppm", std::ios::binary); // save the framebuffer to file ofs << "P6\n" << width << " " << height << "\n255\n"; for (size_t i = 0; i < height*width; ++i) { for (size_t j = 0; j<3; j++) { ofs << (char)(std::max(0, std::min(255, static_cast<int>(255*framebuffer[i][j])))); } } ofs.close(); return 0; } 

La classe des vecteurs réside dans le fichier geometry.h, je ne le décrirai pas ici: d'une part, tout y est trivial, simple manipulation de vecteurs bidimensionnels et tridimensionnels (addition, soustraction, affectation, multiplication par un scalaire, produit scalaire), et d'autre part, gbg l'a déjà décrit en détail dans le cadre d'un cours magistral sur l'infographie.

J'enregistre l'image au format ppm ; C'est le moyen le plus simple d'enregistrer des images, mais ce n'est pas toujours le plus pratique pour une visualisation ultérieure.

Donc, dans la fonction main (), j'ai deux cycles: le deuxième cycle enregistre simplement l'image sur le disque, et le premier cycle traverse tous les pixels de l'image, émet un rayon de la caméra à travers ce pixel, et cherche à voir si ce rayon croise notre sphère.

Attention, l'idée principale de l'article: si dans le dernier article nous avons considéré analytiquement l'intersection d'un rayon et d'une sphère, maintenant je le compte numériquement. L'idée est simple: la sphère a une équation de la forme x ^ 2 + y ^ 2 + z ^ 2 - r ^ 2 = 0; mais en général la fonction f (x, y, z) = x ^ 2 + y ^ 2 + z ^ 2 - r ^ 2 est définie dans tout l'espace. À l'intérieur de la sphère, la fonction f (x, y, z) aura des valeurs négatives et à l'extérieur de la sphère, elle sera positive. Autrement dit, la fonction f (x, y, z) définit la distance (avec un signe!) À notre sphère pour le point (x, y, z). Par conséquent, nous glissons simplement le long du faisceau jusqu'à ce que nous nous ennuyions ou que la fonction f (x, y, z) devienne négative. La fonction sphère_trace () fait exactement cela.

Troisième étape: éclairage primitif


Codons l'éclairage diffus le plus simple, je veux obtenir une telle image à la sortie:



Comme dans l'article précédent, pour faciliter la lecture, j'ai fait une étape = une validation. Les changements peuvent être vus ici .

Pour un éclairage diffus, il ne nous suffit pas de calculer le point d'intersection du faisceau avec la surface, il nous faut connaître le vecteur normal à la surface en ce point. J'ai reçu ce vecteur normal par de simples différences finies dans notre fonction de la distance à la surface:

 Vec3f distance_field_normal(const Vec3f &pos) { const float eps = 0.1; float d = signed_distance(pos); float nx = signed_distance(pos + Vec3f(eps, 0, 0)) - d; float ny = signed_distance(pos + Vec3f(0, eps, 0)) - d; float nz = signed_distance(pos + Vec3f(0, 0, eps)) - d; return Vec3f(nx, ny, nz).normalize(); } 

En principe, bien sûr, puisque nous dessinons une sphère, la normale peut être obtenue beaucoup plus facilement, mais je l'ai fait avec une réserve pour l'avenir.

Quatrième étape: dessinons un motif sur notre sphère


Et dessinons une sorte de modèle dans notre région, par exemple, comme ceci:



Pour ce faire, dans le code précédent, je n'ai changé que deux lignes!

Comment ai-je fait ça? Bien sûr, je n'ai pas de textures. Je viens de prendre la fonction g (x, y, z) = sin (x) * sin (y) * sin (z); il est à nouveau défini dans tout l'espace. Lorsque mon rayon traverse la sphère à un moment donné, la valeur de la fonction g (x, y, z) à ce point définit la couleur du pixel pour moi.

Soit dit en passant, faites attention aux cercles concentriques autour de la sphère - ce sont des artefacts de mon calcul numérique de l'intersection.

Cinquième étape: cartographie des déplacements


Pourquoi ai-je voulu dessiner ce motif? Et il m'aidera à dessiner un tel hérisson:



Là où mon motif était noir, je veux pousser un trou dans notre sphère, et là où il était blanc, au contraire, étirer la bosse.

Pour ce faire, modifiez simplement les trois lignes de notre code:

 float signed_distance(const Vec3f &p) { Vec3f s = Vec3f(p).normalize(sphere_radius); float displacement = sin(16*sx)*sin(16*sy)*sin(16*sz)*noise_amplitude; return p.norm() - (sphere_radius + displacement); } 

Autrement dit, j'ai changé le calcul de la distance à notre surface, la définissant comme x ^ 2 + y ^ 2 + z ^ 2 - r ^ 2 - sin (x) * sin (y) * sin (z). En fait, nous avons défini une fonction implicite .

Sixième étape: une autre fonction implicite


Et pourquoi est-ce que j'évalue le produit des sinus uniquement pour les points se trouvant à la surface de notre sphère? Redéfinissons notre fonction implicite comme ceci:

 float signed_distance(const Vec3f &p) { float displacement = sin(16*px)*sin(16*py)*sin(16*pz)*noise_amplitude; return p.norm() - (sphere_radius + displacement); } 

La différence avec le code précédent est très faible, il vaut mieux voir le diff . Voici le résultat:



Ainsi, nous pouvons définir des composants déconnectés dans notre objet!

Septième étape: bruit pseudo aléatoire


L'image précédente commence déjà à ressembler à une explosion à distance, mais le produit des sinus a un motif trop régulier. Nous aurions besoin de fonctions plus «déchirées», plus «aléatoires» ... Le bruit de Perlin nous sera utile. Voici quelque chose comme ça qui nous conviendrait beaucoup mieux que le produit des sinus:



Comment générer un tel bruit est un peu hors sujet, mais voici l'idée principale: vous devez générer des images aléatoires avec différentes résolutions, les lisser pour obtenir quelque chose comme ceci:



Et puis résumez-les:



En savoir plus ici et ici .

Ajoutons du code qui génère ce bruit et obtenons cette image:



Veuillez noter que dans le code de rendu, je n'ai rien changé du tout, seule la fonction qui «ride» notre sphère a changé.

Étape huit, finale: ajouter de la couleur


La seule chose que j'ai modifiée dans ce commit est qu'au lieu d'une couleur blanche uniforme, j'ai appliqué une couleur qui dépend linéairement de la quantité de bruit appliquée:

 Vec3f palette_fire(const float d) { const Vec3f yellow(1.7, 1.3, 1.0); // note that the color is "hot", ie has components >1 const Vec3f orange(1.0, 0.6, 0.0); const Vec3f red(1.0, 0.0, 0.0); const Vec3f darkgray(0.2, 0.2, 0.2); const Vec3f gray(0.4, 0.4, 0.4); float x = std::max(0.f, std::min(1.f, d)); if (x<.25f) return lerp(gray, darkgray, x*4.f); else if (x<.5f) return lerp(darkgray, red, x*4.f-1.f); else if (x<.75f) return lerp(red, orange, x*4.f-2.f); return lerp(orange, yellow, x*4.f-3.f); } 

Il s'agit d'un simple dégradé linéaire entre les cinq couleurs clés. Eh bien, voici l'image!



Conclusion


Cette technique de lancer de rayons est appelée marche de rayons. Les devoirs sont simples: traversez le ray tracer précédent avec blackjack et reflets avec notre explosion, afin que l'explosion éclaire aussi tout autour! Au fait, cette explosion manque de translucidité.

Source: https://habr.com/ru/post/fr437714/


All Articles