main
creer une image
charger un maillage et ses matières
extraire les sources de lumiere (trouver les triangles dont la matière émet de la lumiere)
pour chaque pixel de l'image :
générer un rayon dans le repere de la scene
// trouver le point de la scène visible pour le rayon
pour chaque triangle :
transformer le rayon dans le repere local de l'objet
calculer l'intersection du rayon avec le triangle,
conserver l'intersection si elle est valide et plus proche que la dernière trouvée
si une intersection valide existe
ecrire un pixel blanc dans l'image
sauver l'image
./bin/shader_kit data/shaders/mesh.glsl mesh.obj
utilisez des points générés aléatoirement à la surface des sources de lumières pour évaluer l'éclairage direct.
rappel :
theta est l'angle entre la normale en p et la direction p vers s,
theta_s est l'angle entre la normale en s et la direction s vers p.
cf GI
compendium eq 18 pour générer des points dans un triangle,
ou cette solution plus récente (et mieux expliquée) : "A
Low-Distortion Map Between Triangle and Square", E. Heitz,
2019
préparation : avant de vous lancer dans la version
"complète", calculez une version simplifiée de l'éclairage global
: l'occultation ambiante
utilisez des directions v uniformes, cf GI compendium eq 34 ou des
directions distribuées selon
,
cf GI compendium eq 35
remarque : les directions alétoires sont générées dans un
repère local, il faut donc les transformer pour connaitre leurs
coordonnées dans le repère du monde. On peut construire le
changement de repère avec la normale du point p et un autre
vecteur (+ 2 produits vectoriels pour construire 2 directions
orthogonales). Il y a quelques subtilités dans cette construction,
autant en utiliser une qui est robuste et rapide :
cf "Generating
a consistently oriented tangent space", Frisvad, 2012,
et "Building an
Orthonormal Basis, Revisited", Pixar, 2017
struct World
{
World( const Vector& _n ) : n(_n)
{
float sign= std::copysign(1.0f, n.z);
float a= -1.0f / (sign + n.z);
float d= n.x * n.y * a;
t= Vector(1.0f + sign * n.x * n.x * a, sign * d, -sign * n.x);
b= Vector(d, sign + n.y * n.y * a, -n.y);
}
// transforme le vecteur du repere local vers le repere du monde
Vector operator( ) ( const Vector& local ) const { return local.x * t + local.y * b + local.z * n; }
// transforme le vecteur du repere du monde vers le repere local
Vector inverse( const Vector& global ) const { return Vector(dot(global, t), dot(global, b), dot(global, n)); }
Vector t;
Vector b;
Vector n;
};
voici quelques résultats pour 1, 4, 16 et 64 directions :
pour vérifier que les directions générées sont correctement
distribuées, vous pouvez utiliser directions.cpp,
il suffit de remplacer les fonctions direction() et pdf() par les
votres et d'inspecter le résultat :
make directions config=release64
bin/directions
bin/image_viewer sphere.hdr density.hdr
Maintenant que vous savez générer des directions bien distribuées
(et dans le repère de la scène), il ne "reste" plus qu'à évaluer
ça :
Commencez par restructurer un peu votre code, pour créer les
fonctions direct( ) et indirect( ). q est le point visible par p
dans la direction v.
Ecrivez l'estimateur Monte Carlo de Lindirect(), quelle
pdf peut-on utiliser pour l'évaluer ?
Comment choisir le nombre d'échantillons utilisé pour évaluer Ldirect()
et Lindirect(), afin de contrôler le temps d'exécution
total de l'image ?
exemple de résultats pour 1, 4, 16 et 64 directions :
colonne de gauche : calculs classiques avec 16 directions,
colonne de droite : mêmes calculs, mais le bruit est différent,
l'erreur est la même... elle est répartie de manière moins visible
/ génante.
cf "Distributing
Monte Carlo Errors as a Blue Noise in Screen Space by Permuting
Pixel Seeds Between Frames", Heitz, Belcour, 2019
int main( const int argc, const char **argv )
{
const char *mesh_filename=
"cornell.obj";
const char *orbiter_filename=
"cornell_orbiter.txt";
if(argc > 1) mesh_filename=
argv[1];
if(argc > 2) orbiter_filename=
argv[2];
printf("%s: '%s' '%s'\n", argv[0],
mesh_filename, orbiter_filename);
// creer l'image resultat
Image image(1024, 640);
// charger un objet
Mesh mesh= read_mesh(mesh_filename);
if(mesh.triangle_count() == 0)
// erreur de
chargement, pas de triangles
return 1;
// creer l'ensemble de triangles /
structure acceleratrice
BVH bvh(mesh);
Sources sources(mesh);
// charger la camera
Orbiter camera;
if(camera.read_orbiter(orbiter_filename))
// erreur,
pas de camera
return 1;
// recupere les transformations
view, projection et viewport pour generer les rayons
Transform model= Identity();
Transform view= camera.view();
Transform projection=
camera.projection(image.width(), image.height(), 45);
Transform viewport=
Viewport(image.width(), image.height());
auto cpu_start=
std::chrono::high_resolution_clock::now();
// parcourir tous les pixels de
l'image
// en parallele avec openMP, un
thread par bloc de 16 lignes
#pragma omp parallel for schedule(dynamic, 1)
for(int py= 0; py <
image.height(); py++)
{
// nombres
aleatoires, version c++11
std::random_device seed;
// un
generateur par thread... pas de synchronisation
std::default_random_engine rng(seed());
// nombres
aleatoires entre 0 et 1
std::uniform_real_distribution<float> u01(0.f, 1.f);
for(int px=
0; px < image.width(); px++)
{
Color color= Black();
// generer le rayon pour le pixel (x, y)
float x= px + u01(rng);
float y= py + u01(rng);
Point o= { ... } // origine dans l'image
Point e= { ... } // extremite dans l'image
Ray ray(o, e);
// calculer les intersections
if(Hit hit= bvh.intersect(ray))
{
const TriangleData& triangle=
mesh.triangle(hit.triangle_id);
// recuperer le triangle
const Material& material=
mesh.triangle_material(hit.triangle_id);
// et sa matiere
Point p= point(hit,
ray);
// point d'intersection
Vector pn= normal(hit,
triangle); // normale
interpolee du triangle au point d'intersection
// retourne la normale pour faire face a la camera / origine du
rayon...
if(dot(pn, ray.d) > 0)
pn= -pn;
// accumuler la couleur de l'echantillon
float cos_theta= std::max(0.f, dot(pn, normalize(-ray.d)));
color= color + 1.f / float(M_PI) * material.diffuse * cos_theta;
}
image(px, py)= Color(color, 1);
}
}
auto cpu_stop=
std::chrono::high_resolution_clock::now();
int cpu_time=
std::chrono::duration_cast<std::chrono::milliseconds>(cpu_stop
- cpu_start).count();
printf("cpu %ds %03dms\n",
int(cpu_time / 1000), int(cpu_time % 1000));
// enregistrer l'image resultat
write_image(image, "render.png");
write_image_hdr(image,
"render.hdr");
return 0;
}