Suivi de chemin GPU Unity - Partie 2

image

"Il n'y a rien de pire qu'une image claire d'un concept flou." - photographe Ansel Adams

Dans la première partie de l'article, nous avons créé un traceur de rayons Whited, capable de tracer des reflets parfaits et des ombres nettes. Mais il nous manque les effets de flou: réflexion diffuse, reflets brillants et ombres douces.

Sur la base du code que nous avons déjà , nous allons résoudre de manière itérative l'équation de rendu formulée par James Cajia en 1986 et transformer notre moteur de rendu en un traceur de chemin capable de transmettre les effets ci-dessus. Nous utiliserons à nouveau C # pour les scripts et HLSL pour les shaders. Le code est téléchargé sur Bitbucket .

Cet article est beaucoup plus mathématique que le précédent, mais ne vous inquiétez pas. Je vais essayer d'expliquer chaque formule aussi clairement que possible. Les formules sont nécessaires ici pour voir ce qui se passe et pourquoi notre moteur de rendu fonctionne, donc je recommande d'essayer de les comprendre et si quelque chose n'est pas clair, posez des questions dans les commentaires de l'article d'origine.

L'image ci-dessous est rendue à l'aide de la carte Graffiti Shelter du site Web HDRI Haven. Les autres images de cet article ont été rendues à l'aide de la carte Kiara 9 Dusk .

image

Équation de rendu


D'un point de vue formel, la tâche du rendu photoréaliste est de résoudre l'équation de rendu, qui s'écrit comme suit:

L(x, vec omegao)=Le(x, vec omegao)+ int Omegafr(x, vec omegai, vec omegao)( vec omegai cdot vecn)L(x, vec omegai)d vec omegai


Analysons-le. Notre objectif ultime est de déterminer la luminosité du pixel de l'écran. L'équation de rendu nous donne la quantité d'éclairage L(x, vec omegao) venant d'un point x (point d'incidence du faisceau) dans la direction  vec omegao (la direction dans laquelle le faisceau tombe). La surface elle-même peut être une source de lumière émettant de la lumière Le(x, vec omegao) dans notre direction. La plupart des surfaces ne le font pas, elles ne réfléchissent donc que la lumière de l'extérieur. C'est pourquoi l'intégrale est utilisée. Il accumule l'éclairage provenant de toutes les directions possibles de l'hémisphère.  Omega autour de la normale (donc, si l'on tient compte de l'éclairage tombant sur la surface par le haut , et non de l'intérieur , ce qui peut être nécessaire pour les matériaux translucides).

La première partie est fr est appelée fonction de distribution de réflectance bidirectionnelle (BRDF). Cette fonction décrit visuellement le type de matériau auquel nous avons affaire: métal ou diélectrique, sombre ou brillant, brillant ou mat. BRDF détermine la proportion d'éclairage provenant de  vec omegai qui se reflète dans la direction  vec omegao . En pratique, cela est mis en œuvre en utilisant un vecteur à trois composantes avec des valeurs de rouge, vert et bleu dans l'intervalle [0,1] .

Deuxième partie - ( vec omegai cdot vecn) Est l'équivalent de 1 cos theta theta - angle entre la lumière incidente et la normale à la surface  vecn . Imaginez une colonne de rayons de lumière parallèles tombant à la surface perpendiculairement. Imaginez maintenant le même rayon tombant à la surface à un angle plat. La lumière sera distribuée sur une plus grande zone, mais cela signifie également que chaque point de cette zone sera plus sombre. Le cosinus est nécessaire pour en tenir compte.

Enfin, l'éclairage lui-même obtenu à partir de  vec omegai est déterminé récursivement en utilisant la même équation. Autrement dit, l'éclairage au point x Dépend de la lumière incidente de toutes les directions possibles dans l'hémisphère supérieur. Dans chacune de ces directions à partir d'un point x il y a un autre point x prime , dont la luminosité dépend à nouveau de la lumière tombant de toutes les directions possibles de l'hémisphère supérieur de ce point. Tous les calculs sont répétés.

Voici ce qui se passe ici, c'est une équation intégrale infiniment récursive avec un nombre infini de régions hémisphériques d'intégration. Nous ne pouvons pas résoudre cette équation directement, mais il existe une solution assez simple.



1 N'oubliez pas! Nous parlerons souvent de cosinus, et nous garderons toujours à l'esprit le produit scalaire. Depuis  veca cdot vecb= | veca |  | vecb | cos( theta) , et nous avons affaire à des directions (vecteurs unitaires), alors le produit scalaire est le cosinus dans la plupart des tâches d'infographie.

Monte Carlo vient à la rescousse


L'intégration de Monte Carlo est une technique d'intégration numérique qui nous permet de calculer approximativement toute intégrale à l'aide d'un nombre fini d'échantillons aléatoires. De plus, Monte Carlo garantit la convergence vers la bonne décision - plus nous prélevons d'échantillons, mieux c'est. Voici sa forme généralisée:

FN approx frac1N sumNn=0 fracf(xn)p(xn)


Par conséquent, l'intégrale de la fonction f(xn) peut être calculé approximativement en faisant la moyenne des échantillons aléatoires dans le domaine d'intégration. Chaque échantillon est divisé par la probabilité de sa sélection. p(xn) . Pour cette raison, l'échantillon le plus souvent choisi aura plus de poids que l'échantillon le moins souvent choisi.

Dans le cas d'échantillons uniformes dans l'hémisphère (chaque direction a la même probabilité d'être sélectionnée), la probabilité d'échantillons est constante: p( omega)= frac12 pi (car 2 pi Est la surface d'un seul hémisphère). Si nous mettons tout cela ensemble, nous obtenons ce qui suit:

L(x, vec omegao) approxLe(x, vec omegao)+ frac1N sumNn=0 colorVert2 pifr(x, vec omegai, vec omegao)( vec omegai cdot vecn)L(x, vec omegai)


Le rayonnement Le(x, vec omegao) Est juste la valeur retournée par notre fonction Shade .  frac1N déjà en cours d'exécution dans notre fonction AddShader . Multiplication par L(x, vec omegai) se produit lorsque nous réfléchissons le rayon et le traçons plus loin. Notre tâche est de donner vie à la partie verte de l'équation.

Prérequis


Avant de se lancer dans un voyage, prenons soin de certains aspects: accumulation d'échantillons, scènes déterministes et aléatoire des shaders.

Accumulation


Pour une raison quelconque, Unity ne me transmet pas la texture HDR comme destination dans OnRenderImage . Le format R8G8B8A8_Typeless a fonctionné pour moi, donc la précision devient rapidement trop faible pour accumuler un grand nombre d'échantillons. Pour gérer cela, ajoutons private RenderTexture _converged au private RenderTexture _converged C # private RenderTexture _converged . Ce sera notre tampon, accumulant avec une grande précision les résultats avant de les afficher à l'écran. Nous initialisons / libérons la texture de la même manière que _target dans la fonction InitRenderTexture . Dans la fonction Render , doublez le blitting:

 Graphics.Blit(_target, _converged, _addMaterial); Graphics.Blit(_converged, destination); 

Scènes déterministes


Lorsque vous modifiez le rendu pour évaluer l'effet, il est utile de comparer avec les résultats précédents. Jusqu'à présent, à chaque redémarrage du mode Play ou recompilation du script, nous aurons une nouvelle scène aléatoire. Pour éviter cela, ajoutez le public int SphereSeed au public int SphereSeed C # et la ligne suivante au début de SetUpScene :

 Random.InitState(SphereSeed); 

Maintenant, nous pouvons définir manuellement les scènes de départ. Entrez un nombre et activez / RayTracingMaster jusqu'à ce que vous obteniez la bonne scène.

Les paramètres suivants ont été utilisés pour des exemples d'images: Sphere Seed 1223832719, Sphere Radius [5, 30], Spheres Max 10000, Sphere Placement Radius 100.

Aléatoire du shader


Avant de commencer l'échantillonnage stochastique, nous devons ajouter un caractère aléatoire au shader. J'utiliserai la chaîne canonique que j'ai trouvée sur le réseau, modifiée pour plus de commodité:

 float2 _Pixel; float _Seed; float rand() { float result = frac(sin(_Seed / 100.0f * dot(_Pixel, float2(12.9898f, 78.233f))) * 43758.5453f); _Seed += 1.0f; return result; } 

Initialisez _Pixel directement dans CSMain comme _Pixel = id.xy afin que chaque pixel puisse utiliser différentes valeurs aléatoires. _Seed initialisé à partir de C # dans la fonction SetShaderParameters .

 RayTracingShader.SetFloat("_Seed", Random.value); 

La qualité des nombres aléatoires générés ici est instable. À l'avenir, il serait intéressant d'explorer et de tester cette fonction en analysant l'influence des paramètres et en la comparant à d'autres approches. Mais pour l'instant, nous allons simplement l'utiliser et espérer pour le mieux.

Échantillonnage hémisphérique


Commençons encore: nous avons besoin de directions aléatoires uniformément réparties dans l'hémisphère. Cette tâche non triviale pour l'étendue complète est décrite en détail dans cet article par Corey Simon. Il est facile de s'adapter à l'hémisphère. Voici à quoi ressemble le code du shader:

 float3 SampleHemisphere(float3 normal) { //     float cosTheta = rand(); float sinTheta = sqrt(max(0.0f, 1.0f - cosTheta * cosTheta)); float phi = 2 * PI * rand(); float3 tangentSpaceDir = float3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta); //      return mul(tangentSpaceDir, GetTangentSpace(normal)); } 

Les directions sont générées pour un hémisphère centré sur l'axe Z positif, nous devons donc les transformer pour qu'elles soient centrées sur la normale souhaitée. Nous générons une tangente et binormale (deux vecteurs orthogonaux à la normale et orthogonaux l'un à l'autre). Tout d'abord, nous sélectionnons un vecteur auxiliaire pour générer la tangente. Pour ce faire, nous prenons l'axe X positif et ne revenons au Z positif que s'il est normalement (approximativement) aligné avec l'axe X. Ensuite, nous pouvons utiliser le produit vectoriel pour générer la tangente, puis le binormal.

 float3x3 GetTangentSpace(float3 normal) { //       float3 helper = float3(1, 0, 0); if (abs(normal.x) > 0.99f) helper = float3(0, 0, 1); //   float3 tangent = normalize(cross(normal, helper)); float3 binormal = normalize(cross(normal, tangent)); return float3x3(tangent, binormal, normal); } 

Diffusion de Lambert


Maintenant que nous avons des directions aléatoires uniformes, nous pouvons procéder à la mise en œuvre du premier BRDF. Pour la réflexion diffuse, le plus couramment utilisé est le Lambert BRDF, qui est étonnamment simple: fr(x, vec omegai, vec omegao)= frackd pikd - C'est la surface de l'albédo. Insérons-le dans notre équation de rendu Monte Carlo (je ne prendrai pas encore en compte l'émissivité) et voyons ce qui se passe:

L(x, vec omegao) approx frac1N sumNn=0 colorBlueViolet2kd( vec omegai cdot vecn)L(x, vec omegai)


Insérons immédiatement cette équation dans le shader. Dans la fonction Shade , remplacez le code à l'intérieur de la construction if (hit.distance < 1.#INF) par les lignes suivantes:

 //   ray.origin = hit.position + hit.normal * 0.001f; ray.direction = SampleHemisphere(hit.normal); ray.energy *= 2 * hit.albedo * sdot(hit.normal, ray.direction); return 0.0f; 

La nouvelle direction du faisceau réfléchi est déterminée en utilisant notre fonction d'échantillons d'hémisphère homogènes. L'énergie du faisceau est multipliée par la partie correspondante de l'équation ci-dessus. Étant donné que la surface n'émet aucun éclairage (elle ne reflète que la lumière reçue directement ou indirectement du ciel), nous AddShader 0. Ici, n'oubliez pas que AddShader fait la moyenne des échantillons, donc nous n'avons pas à nous soucier de  frac1N sum . CSMain contient déjà la multiplication par L(x, vec omegai) (le prochain faisceau réfléchi), nous n'avons donc plus beaucoup de travail.

sdot est une fonction d'aide que j'ai définie pour moi-même. Il renvoie simplement le résultat du produit scalaire avec un coefficient supplémentaire, puis le limite à l'intervalle [0,1] :

 float sdot(float3 x, float3 y, float f = 1.0f) { return saturate(dot(x, y) * f); } 

Résumons ce que notre code fait jusqu'à présent. CSMain génère les rayons primaires de la caméra et appelle Shade . En traversant la surface, cette fonction génère à son tour un nouveau faisceau (uniformément aléatoire dans l'hémisphère autour de la normale) et prend en compte le BRDF du matériau et le cosinus dans l'énergie du faisceau. À l'intersection du rayon avec le ciel, nous échantillonnons HDRI (notre seule source d'éclairage) et renvoyons l'illumination, qui est multipliée par l'énergie du rayon (c'est-à-dire le résultat de toutes les intersections précédentes, à partir de la caméra). Il s'agit d'un échantillon simple qui se mélange à un résultat convergent. Par conséquent, l'impact est pris en compte dans chaque échantillon.  frac1N .

Il est temps de tout vérifier au travail. Étant donné que les métaux n'ont pas de réflexion diffuse, désactivons-les pour l'instant dans la fonction SetUpScene d'un script C # (mais appelez toujours Random.value ici pour préserver le déterminisme de la scène):

 bool metal = Random.value < 0.0f; 

Lancez le mode Lecture et voyez comment l'image initialement bruyante est effacée et converge vers un beau rendu:

Image miroir de Phong


Pas mal pour seulement quelques lignes de code (et une petite fraction des calculs). Affinons l'image en ajoutant des reflets miroir à l'aide du BRDF de Phong. La formulation originale de Fong avait ses problèmes (manque de relations et de conservation d'énergie), mais heureusement, d' autres personnes les ont éliminés . Le BRDF amélioré est illustré ci-dessous.  vec omegar Est la direction de la lumière parfaitement réfléchie, et  alpha Est un indicateur Phong qui contrôle la rugosité:

fr(x, vec omegai, vec omegao)=ks frac alpha+22 pi( vec omegar cdot vec omegao) alpha


Un graphique interactif à deux dimensions montre à quoi ressemble le BRDF pour Phong lorsque  alpha=15 pour un faisceau incident à un angle de 45 °. Essayez de modifier la valeur.  alpha .

Collez ceci dans notre équation de rendu Monte Carlo:

L(x, vec omegao) approx frac1N sumNn=0 colorbrownks( alpha+2)( vec omegar cdot vec omegao) alpha( vec omegai cdot vecn)L(x, vec omegai)


Et enfin, ajoutons ceci au Lambert BRDF existant:

L(x, vec omegao) approx frac1N sumNn=0[ colorBlueViolet2kd+ colorbrownks( alpha+2)( vec omegar cdot vec omegao) alpha]( vec omegai cdot vecn)L(x, vec omegai)


Et voici à quoi ils ressemblent dans le code avec la diffusion Lambert:

 //    ray.origin = hit.position + hit.normal * 0.001f; float3 reflected = reflect(ray.direction, hit.normal); ray.direction = SampleHemisphere(hit.normal); float3 diffuse = 2 * min(1.0f - hit.specular, hit.albedo); float alpha = 15.0f; float3 specular = hit.specular * (alpha + 2) * pow(sdot(ray.direction, reflected), alpha); ray.energy *= (diffuse + specular) * sdot(hit.normal, ray.direction); return 0.0f; 

Notez que nous avons remplacé le produit scalaire par un produit légèrement différent, mais équivalent (reflété  omegao au lieu de  omegai ) Maintenant, remettez les matériaux métalliques dans les fonctions SetUpScene et vérifiez comment cela fonctionne.

Expérimenter avec différentes valeurs  alpha , vous pouvez remarquer un problème: même de faibles performances nécessitent beaucoup de temps pour la convergence, et à hautes performances, le bruit est particulièrement frappant. Même après quelques minutes d'attente, le résultat est loin d'être idéal, ce qui est inacceptable pour une scène aussi simple.  alpha=15 et  alpha=300 avec 8192 échantillons ressemblent à ceci:



Pourquoi est-ce arrivé? Après tout, avant d'avoir de si beaux reflets idéaux (  alpha= infty )! .. Le problème est que nous générons des échantillons homogènes et leur attribuons des poids selon BRDF. Avec des valeurs Phong élevées, le BRDF est petit pour tout le monde, mais ces directions sont très proches d'une réflexion parfaite, et il est très peu probable que nous les sélectionnions au hasard en utilisant nos échantillons homogènes . D'un autre côté, si nous traversons réellement l'une de ces directions, alors le BRDF sera énorme pour compenser tous les autres petits échantillons. Le résultat est une très grande dispersion. Les chemins avec de multiples réflexions spéculaires sont encore pires et entraînent un bruit visible sur les images.

Échantillonnage amélioré


Pour rendre notre traceur de chemin pratique, nous devons changer le paradigme. Au lieu de gaspiller des échantillons précieux sur des zones dans lesquelles ils finissent par être sans importance (car ils obtiennent une très faible valeur BRDF et / ou cosinus), générons des échantillons importants .

Dans un premier temps, nous reviendrons sur nos réflexions idéales, puis verrons comment cette idée peut être généralisée. Pour ce faire, nous divisons la logique d'ombrage en réflexion diffuse et spéculaire. Pour chaque échantillon, nous choisirons au hasard l'un ou l'autre (en fonction du ratio kd et ks ) Dans le cas d'une réflexion diffuse, nous adhérerons à des échantillons homogènes, mais pour le spéculaire, nous réfléchirons explicitement le faisceau dans la seule direction importante. Étant donné que moins d'échantillons seront désormais dépensés pour chaque type de réflexion, nous devons augmenter l'influence en conséquence, afin d'obtenir la même valeur totale:

 //       hit.albedo = min(1.0f - hit.specular, hit.albedo); float specChance = energy(hit.specular); float diffChance = energy(hit.albedo); float sum = specChance + diffChance; specChance /= sum; diffChance /= sum; //     float roulette = rand(); if (roulette < specChance) { //   ray.origin = hit.position + hit.normal * 0.001f; ray.direction = reflect(ray.direction, hit.normal); ray.energy *= (1.0f / specChance) * hit.specular * sdot(hit.normal, ray.direction); } else { //   ray.origin = hit.position + hit.normal * 0.001f; ray.direction = SampleHemisphere(hit.normal); ray.energy *= (1.0f / diffChance) * 2 * hit.albedo * sdot(hit.normal, ray.direction); } return 0.0f; 

energy est une petite fonction d'aide qui fait la moyenne des canaux de couleur:

 float energy(float3 color) { return dot(color, 1.0f / 3.0f); } 

Nous avons donc créé un traceur de rayons Whited plus beau à partir de la partie précédente, mais maintenant avec un véritable ombrage diffus (ce qui implique des ombres douces, une occlusion ambiante, un éclairage global diffus):

image

Échantillon d'importance


Jetons un autre regard sur la formule de base de Monte Carlo:

FN approx frac1N sumNn=0 fracf(xn)p(xn)


Comme vous pouvez le voir, nous divisons l'influence de chaque échantillon (échantillon) sur la probabilité de choisir cet échantillon particulier. Jusqu'à présent, nous avons utilisé des échantillons d'hémisphère homogènes, nous avons donc eu une constante p( omega)= frac12 pi . Comme nous l'avons vu plus haut, cela est loin d'être optimal, par exemple dans le cas du Phong BRDF, qui est grand dans un très petit nombre de directions.

Imaginez que nous puissions trouver une distribution de probabilité correspondant exactement à la fonction intégrable: p(x)=f(x) . Ensuite, les événements suivants se produiront:

FN approx frac1N sumNn=01


Maintenant, nous n'avons plus d'échantillons qui apportent très peu de contribution. Ces échantillons seront moins susceptibles d'être sélectionnés. Cela réduira considérablement la variance du résultat et accélérera la convergence du rendu.

En pratique, il est impossible de trouver une telle distribution idéale, car certaines parties de la fonction intégrable (dans notre cas BRDF × cosinus × lumière incidente) sont inconnues (c'est le plus évident pour la lumière incidente), mais la distribution des échantillons selon BRDF × cosinus ou même seulement selon BRDF aidera nous. Ce principe est appelé échantillonnage par importance.

Échantillon de cosinus


Dans les étapes suivantes, nous devons remplacer notre distribution homogène d'échantillons par la distribution selon la règle du cosinus. N'oubliez pas, au lieu de multiplier les échantillons homogènes par cosinus, en réduisant leur influence, nous voulons générer un nombre d'échantillons proportionnellement plus petit .

Cet article de Thomas Poole décrit comment procéder. Nous ajouterons le paramètre alpha à notre fonction SampleHemisphere . La fonction détermine l'indice de sélection des cosinus: 0 pour un échantillon uniforme, 1 pour la sélection des cosinus, ou supérieur pour les valeurs supérieures de Phong. En code, cela ressemble à ceci:

 float3 SampleHemisphere(float3 normal, float alpha) { //  ,      float cosTheta = pow(rand(), 1.0f / (alpha + 1.0f)); float sinTheta = sqrt(1.0f - cosTheta * cosTheta); float phi = 2 * PI * rand(); float3 tangentSpaceDir = float3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta); //      return mul(tangentSpaceDir, GetTangentSpace(normal)); } 

Maintenant, la probabilité de chaque échantillon est égale p( omega)= frac alpha+12 pi( vec omega cdot vecn) alpha . La beauté de cette équation peut ne pas sembler immédiatement évidente, mais un peu plus tard, vous la comprendrez.

Échantillon Lambert par importance


Pour commencer, nous allons affiner le rendu de réflexion diffuse. Dans notre distribution homogène, la constante de Lambert BRDF est déjà utilisée, mais nous pouvons l'améliorer en ajoutant le cosinus. La distribution de probabilité de l'échantillon par cosinus (où  alpha=1 ) est égal  frac( vec omegai cdot vecn) pi , ce qui simplifie notre formule de Monte Carlo pour la réflexion diffuse:

L(x, vec omegao) approx frac1N sumNn=0 colorBlueVioletkdL(x, vec omegai)


 //   ray.origin = hit.position + hit.normal * 0.001f; ray.direction = SampleHemisphere(hit.normal, 1.0f); ray.energy *= (1.0f / diffChance) * hit.albedo; 

Cela accélérera un peu notre ombrage diffus. Passons maintenant au vrai problème.

Échantillonnage de Fongov par importance


Pour Phong BRDF, la procédure est similaire. Cette fois, nous avons le produit de deux cosinus: le cosinus standard de l'équation de rendu (comme dans le cas de la réflexion diffuse), multiplié par le cosinus BRDF propre. Nous ne traiterons que du dernier.

Insérons la distribution de probabilité des exemples ci-dessus dans l'équation de Phong. Une conclusion détaillée peut être trouvée dans Lafortune et Willems: Using the Modified Phong Reflectance Model for Physically Based Rendering (1994) :

L(x, vec omegao) approx frac1N sumNn=0 colorbrownks frac alpha+2 alpha+1( v e c o m e g a i c d o t v e c n )    L ( x , v e c o m e g a i ) 


 //   float alpha = 15.0f; ray.origin = hit.position + hit.normal * 0.001f; ray.direction = SampleHemisphere(reflect(ray.direction, hit.normal), alpha); float f = (alpha + 2) / (alpha + 1); ray.energy *= (1.0f / specChance) * hit.specular * sdot(hit.normal, ray.direction, f); 

Ces changements sont suffisants pour éliminer tout problème de hautes performances dans Phong et faire converger notre rendu dans un délai beaucoup plus raisonnable.

Matériaux


Enfin, élargissons notre génération de scènes pour créer des valeurs changeantes pour la douceur et l'émissivité des sphères! Dans la struct Sphere partir d'un script C #, ajoutez public float smoothness public Vector3 emission et public Vector3 emission . Puisque nous avons changé la taille de la structure, nous devons changer l'étape lors de la création du tampon de calcul (4 × le nombre de nombres flottants, vous vous souvenez?). Faites en SetUpScene fonction SetUpScene insère des valeurs de douceur et d' SetUpScene .

Dans le shader, ajoutez les deux variables à struct Sphere et struct RayHit , puis initialisez-les dans CreateRayHit . Et enfin, définissez les deux valeurs dans IntersectGroundPlane (codé en dur, collez toutes les valeurs) et IntersectSphere (obtention des valeurs de Sphere ).

Je veux utiliser les valeurs de lissage de la même manière que dans le shader Unity standard, qui diffère d'un exposant de Fong plutôt arbitraire. Voici une bonne conversion qui peut être utilisée dans la fonction Shade :

 float SmoothnessToPhongAlpha(float s) { return pow(1000.0f, s * s); } 

 float alpha = SmoothnessToPhongAlpha(hit.smoothness); 



L'utilisation de l'émissivité se fait en renvoyant une valeur dans Shade :

 return hit.emission; 

Résultats


Respirez profondément. détendez-vous et attendez que l'image se transforme en une si belle image:

image

Félicitations! Vous avez réussi à traverser le fourré d'expressions mathématiques. Nous avons implémenté un traceur de chemin qui effectue un ombrage diffus et miroir, a appris l'échantillonnage par importance, en appliquant immédiatement ce concept afin que le rendu converge en quelques minutes, pas en heures ou en jours.

Par rapport au précédent, cet article a été une étape énorme en termes de complexité, mais a également considérablement amélioré la qualité du résultat. Travailler avec des calculs mathématiques prend du temps, mais il se justifie car il peut approfondir considérablement votre compréhension de ce qui se passe et vous permettra d'étendre l'algorithme sans détruire la fiabilité physique.

Merci d'avoir lu! Dans la troisième partie, nous quitterons (pendant un certain temps) la jungle de l'échantillonnage et de l'ombrage, et retournerons dans la civilisation à la rencontre des messieurs Moller et Trumbor. Nous devrons leur parler des triangles.

À propos de l'auteur: David Curie est le développeur de Three Eyed Games, programmeur du laboratoire d'ingénierie virtuelle de Volkswagen, chercheur en infographie et musicien de heavy metal.

image

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


All Articles