Dibujamos una explosi贸n de dibujos animados para 180 l铆neas de C ++ desnudo

Hace una semana, publiqu茅 otro cap铆tulo de mi curso de conferencia de gr谩ficos por computadora ; Hoy, volvemos nuevamente al trazado de rayos, pero esta vez iremos un poco m谩s all谩 de la representaci贸n de esferas triviales. No necesito fotorrealismo; para fines de dibujos animados, tal explosi贸n , me parece, se reducir谩.

Como siempre, solo tenemos un compilador desnudo a nuestra disposici贸n, no se pueden usar bibliotecas de terceros. No quiero molestarme con los gestores de ventanas, el procesamiento del mouse / teclado y similares. El resultado de nuestro programa ser谩 una simple imagen guardada en el disco. No persigo la velocidad / optimizaci贸n en absoluto, mi objetivo es mostrar los principios b谩sicos.

En total, 驴c贸mo dibujar esa imagen en 180 l铆neas de c贸digo en tales condiciones?



Perm铆tanme incluso insertar un gif animado (seis metros):



Y ahora dividiremos toda la tarea en varias etapas:

Etapa uno: lee el art铆culo anterior


Si exactamente. Lo primero que debe hacer es leer el cap铆tulo anterior , que habla sobre los conceptos b谩sicos del trazado de rayos. Es muy corto, en principio, todas las reflexiones-refracciones no se pueden leer, pero al menos hasta una iluminaci贸n difusa, recomiendo leerlo. El c贸digo es bastante simple, la gente incluso lo ejecuta en microcontroladores:



Etapa dos: dibuja una esfera


Dibujemos una esfera sin molestarnos con materiales o iluminaci贸n. Por simplicidad, esta esfera vivir谩 en el centro de coordenadas. Sobre esta foto quiero obtener:



Vea el c贸digo aqu铆 , pero d茅jeme darle el principal directamente en el texto del art铆culo:

#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 clase de vectores vive en el archivo geometry.h, no lo describir茅 aqu铆: en primer lugar, todo es trivial all铆, la manipulaci贸n simple de vectores de dos y tres dimensiones (suma, resta, asignaci贸n, multiplicaci贸n por un producto escalar, escalar), y en segundo lugar, gbg ya lo describi贸 en detalle como parte de un curso de conferencias sobre gr谩ficos por computadora.

Guardo la imagen en formato ppm ; Esta es la forma m谩s f谩cil de guardar im谩genes, aunque no siempre es la m谩s conveniente para su posterior visualizaci贸n.

Entonces, en la funci贸n main (), tengo dos ciclos: el segundo ciclo simplemente guarda la imagen en el disco, y el primer ciclo pasa a trav茅s de todos los p铆xeles de la imagen, emite un rayo desde la c谩mara a trav茅s de este p铆xel, y mira para ver si este rayo se cruza con nuestra esfera.

Atenci贸n, la idea principal del art铆culo: si en el 煤ltimo art铆culo consideramos anal铆ticamente la intersecci贸n de un rayo y una esfera, ahora lo cuento num茅ricamente. La idea es simple: la esfera tiene una ecuaci贸n de la forma x ^ 2 + y ^ 2 + z ^ 2 - r ^ 2 = 0; pero en general la funci贸n f (x, y, z) = x ^ 2 + y ^ 2 + z ^ 2 - r ^ 2 se define en todo el espacio. Dentro de la esfera, la funci贸n f (x, y, z) tendr谩 valores negativos, y fuera de la esfera, ser谩 positiva. Es decir, la funci贸n f (x, y, z) establece la distancia (隆con un signo!) A nuestra esfera para el punto (x, y, z). Por lo tanto, simplemente nos deslizamos a lo largo de la viga hasta que nos aburrimos o la funci贸n f (x, y, z) se vuelve negativa. La funci贸n sphere_trace () hace exactamente eso.

Etapa tres: iluminaci贸n primitiva


Codifiquemos la iluminaci贸n difusa m谩s simple, quiero obtener una imagen de este tipo en la salida:



Como en el art铆culo anterior, para facilitar la lectura, hice un paso = una confirmaci贸n. Los cambios se pueden ver aqu铆 .

Para una iluminaci贸n difusa, no es suficiente para nosotros calcular el punto de intersecci贸n del haz con la superficie, necesitamos conocer el vector normal de la superficie en este punto. Recib铆 este vector normal por diferencias finitas simples en nuestra funci贸n de la distancia a la superficie:

 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 principio, por supuesto, dado que estamos dibujando una esfera, lo normal se puede obtener mucho m谩s f谩cilmente, pero lo hice con una reserva para el futuro.

Etapa cuatro: dibujemos un patr贸n en nuestra esfera


Y dibujemos alg煤n tipo de patr贸n en nuestra 谩rea, por ejemplo, as铆:



Para hacer esto, en el c贸digo anterior, 隆cambi茅 solo dos l铆neas!

驴C贸mo hice esto? Por supuesto, no tengo texturas. Acabo de tomar la funci贸n g (x, y, z) = sin (x) * sin (y) * sin (z); se define nuevamente en todo el espacio. Cuando mi rayo cruza la esfera en alg煤n punto, entonces el valor de la funci贸n g (x, y, z) en este punto establece el color del p铆xel para m铆.

Por cierto, preste atenci贸n a los c铆rculos conc茅ntricos alrededor de la esfera: estos son artefactos de mi c谩lculo num茅rico de la intersecci贸n.

Paso cinco: mapeo de desplazamiento


驴Por qu茅 quer铆a dibujar este patr贸n? Y 茅l me ayudar谩 a dibujar ese erizo:



Donde mi patr贸n era negro, quiero hacer un agujero en nuestra esfera, y donde era blanco, por el contrario, estirar la joroba.

Para hacer esto, simplemente cambie las tres l铆neas en nuestro c贸digo:

 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); } 

Es decir, cambi茅 el c谩lculo de la distancia a nuestra superficie, defini茅ndolo como x ^ 2 + y ^ 2 + z ^ 2 - r ^ 2 - sin (x) * sin (y) * sin (z). De hecho, definimos una funci贸n impl铆cita .

Paso seis: otra funci贸n impl铆cita


驴Y por qu茅 estoy evaluando el producto de los senos solo para los puntos que se encuentran en la superficie de nuestra esfera? Vamos a redefinir nuestra funci贸n impl铆cita de esta manera:

 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 diferencia con el c贸digo anterior es muy peque帽a, es mejor ver la diferencia . Aqu铆 est谩 el resultado:



Por lo tanto, podemos definir componentes desconectados en nuestro objeto.

Paso siete: ruido pseudoaleatorio


La imagen anterior ya est谩 comenzando a parecerse remotamente a una explosi贸n, pero el producto de los senos tiene un patr贸n demasiado regular. Necesitar铆amos una funci贸n m谩s "desgarrada", m谩s "aleatoria" ... El ruido de Perlin vendr谩 en nuestra ayuda. Aqu铆 hay algo como esto que nos convendr铆a mucho mejor que el producto de los senos:



C贸mo generar ese ruido es un poco extra帽o, pero esta es la idea principal: debe generar im谩genes aleatorias con diferentes resoluciones, suavizarlas para obtener algo como esto:



Y luego simplemente resumirlos:



Lea m谩s aqu铆 y aqu铆 .

Agreguemos un c贸digo que genera este ruido y obtengamos esta imagen:



Tenga en cuenta que en el c贸digo de representaci贸n no cambi茅 nada, solo cambi贸 la funci贸n que "arruga" nuestra esfera.

Etapa ocho, final: agregar color


Lo 煤nico que cambi茅 en este commit es que, en lugar de un color blanco uniforme, apliqu茅 un color que depende linealmente de la cantidad de ruido aplicado:

 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); } 

Este es un gradiente lineal simple entre los cinco colores clave. Bueno, aqu铆 est谩 la foto!



Conclusi贸n


Esta t茅cnica de trazado de rayos se llama marcha de rayos. La tarea es simple: cruce el trazador de rayos anterior con blackjack y reflejos con nuestra explosi贸n, 隆para que la explosi贸n tambi茅n ilumine todo a su alrededor! Por cierto, esta explosi贸n carece de translucidez.

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


All Articles