electricsquare/raymarching-workshop

Présenté par Electric Square

Créé et présenté par AJ Weeks & Huw Bowles

Aperçu

Le rendu d’une image implique de déterminer la couleur de chaque pixel de l’image, ce qui nécessite de déterminer quelle surface se trouve derrière le pixel dans le monde, puis de l’ombrer pour calculer une couleur finale.

Les GPU de génération actuelle prennent des maillages triangulaires en entrée, les pixellisent en pixels (appelés fragments avant qu’ils ne soient dessinés sur un écran), puis les ombrent pour calculer leur contribution à l’image. Bien que ce pipeline soit actuellement omniprésent, il est également compliqué et pas nécessairement le meilleur moyen d’apprendre les graphiques.

Une autre approche consiste à projeter un rayon à travers chaque pixel et à le croiser avec les surfaces de la scène, puis à calculer l’ombrage.

Ce cours introduit une technique de diffusion de rayons à travers des  » champs de distance « . Un champ de distance est une fonction qui renvoie la proximité d’un point donné avec la surface la plus proche de la scène. Cette distance définit le rayon d’une sphère d’espace vide autour de chaque point. Les champs de distance signés (SDF) sont des champs de distance définis à la fois à l’intérieur et à l’extérieur des objets ; si la position interrogée est « à l’intérieur » d’une surface, sa distance sera signalée comme négative, sinon elle sera positive.

Qu’est-ce qui est possible avec ray marching?

Le jeu ‘Claybook’ utilise uniquement des champs de distance pour représenter la scène. Cela lui offre de nombreuses possibilités intéressantes, comme des topologies de surface complètement dynamiques et un morphing de forme. Ces effets seraient très difficiles à obtenir avec des mailles triangulaires. D’autres avantages incluent des ombres douces faciles à mettre en œuvre et de haute qualité et une occlusion ambiante.

https://www.claybookgame.com/

L’image suivante a également été rendue en temps réel en utilisant les techniques que nous couvrirons aujourd’hui (ainsi que de nombreuses techniques fantaisistes dans lesquelles nous n’aurons pas le temps de plonger).

Vous pouvez l’exécuter en direct dans votre navigateur ici: https://www.shadertoy.com/view/ld3Gz2

En utilisant un SDF (champ de distance signé), la géométrie de cette scène n’a pas besoin d’être créée dans un DCC comme Maya, mais est représentée entièrement paramétriquement. Cela rend trivial l’animation de la forme en faisant simplement varier les entrées de la fonction de mappage de scène.

D’autres effets graphiques sont simplifiés par raymarching par rapport aux alternatives de rastérisation traditionnelles. La diffusion souterraine, par exemple, nécessite simplement d’envoyer quelques rayons supplémentaires à la surface pour voir son épaisseur. L’occlusion ambiante, l’anticrénelage et la profondeur de champ sont trois autres techniques qui ne nécessitent que quelques lignes supplémentaires et améliorent considérablement la qualité de l’image.

Champs de distance rayonnants

Nous marcherons le long de chaque rayon et chercherons une intersection avec une surface dans la scène. Une façon de le faire serait de commencer à l’origine du rayon (sur le plan de la caméra), et de faire des pas uniformes le long du rayon, en évaluant le champ de distance en chaque point. Lorsque la distance à la scène est inférieure à une valeur de seuil, nous savons que nous avons touché une surface et nous pouvons donc terminer le raymarch et ombrer ce pixel.

Une approche plus efficace consiste à utiliser la distance renvoyée par le SDF pour déterminer la taille de l’étape suivante. Comme mentionné ci-dessus, la distance renvoyée par un SDF peut être considérée comme le rayon d’une sphère d’espace vide autour du point d’entrée. Il est donc prudent de marcher de cette quantité le long du rayon car nous savons que nous ne traverserons aucune surface.

Dans la représentation 2D suivante de raymarching, le centre de chaque cercle est l’endroit d’où la scène a été échantillonnée. Le rayon a ensuite été parcouru le long de cette distance (s’étendant jusqu’au rayon du cercle), puis rééchantillonné.

Comme vous pouvez le voir, l’échantillonnage du SDF ne vous donne pas le point d’intersection exact de votre rayon, mais plutôt une distance minimale que vous pouvez parcourir sans traverser une surface.

Une fois que cette distance est inférieure à un certain seuil, le raymarch termine le pixel peut être ombré en fonction des propriétés de la surface intersectée.

Jouez avec ce shader dans votre navigateur ici: (cliquez et faites glisser dans l’image pour définir la direction des rayons) https://www.shadertoy.com/view/lslXD8

Comparaison avec le traçage de rayons

À ce stade, on peut se demander pourquoi nous ne calculons pas simplement l’intersection avec la scène directement en utilisant des mathématiques analytiques, en utilisant une technique appelée Traçage de rayons. C’est ainsi que les rendus hors ligne fonctionneraient généralement – tous les triangles de la scène sont indexés dans une sorte de structure de données spatiales comme une Hiérarchie de volumes limites (BVH) ou un arbre kD, qui permettent une intersection efficace des triangles situés le long d’un rayon.

Nous avons plutôt des champs de distance raymarch car:

  • Il est très simple de mettre en œuvre la routine de coulée de rayons
  • Nous évitons toute la complexité de la mise en œuvre des intersections de triangles de rayons et des structures de données BVH
  • Nous n’avons pas besoin de créer la représentation explicite de la scène – maillages triangulaires, coordonnées tex, couleurs, etc.
  • Nous bénéficions d’une gamme de fonctionnalités utiles des champs de distance, dont certaines sont mentionnées ci-dessus

Cela dit, il existe des points d’entrée élégants / simples dans le traçage de rayons. Le livre gratuit Ray Tracing in One Weekend (et les chapitres suivants) sont très fortement recommandés et constituent une lecture essentielle pour toute personne intéressée par les graphiques.

Commençons!

ShaderToy

ShaderToy est un site Web et une plate-forme de création de shaders pour naviguer, partager et discuter des shaders.

Bien que vous puissiez entrer directement et commencer à écrire un nouveau shader sans créer de compte, cela est dangereux car vous pouvez facilement perdre du travail en cas de problèmes de connexion ou si vous bloquez le GPU (facilement en créant par exemple une boucle infinie).Par conséquent, nous vous recommandons fortement de créer un compte (c’est rapide / facile / gratuit) en cliquant ici: https://www.shadertoy.com/signin, et en économisant régulièrement.

Pour un aperçu de ShaderToy et un guide de démarrage, nous vous recommandons de suivre un tutoriel tel que celui-ci de @The_ArtOfCode: https://www.youtube.com/watch?v=u5HAYVHsasc. Les bases ici sont nécessaires pour suivre le reste de l’atelier.

Démo SDF 2D

Nous fournissons un cadre simple pour définir et visualiser les champs de distance signés en 2D.

https://www.shadertoy.com/view/Wsf3Rj

Avant de définir le champ de distance, le résultat sera entièrement blanc. Le but de cette section est de concevoir un SDF qui donne la forme de scène souhaitée (contour blanc). Dans le code, cette distance est calculée par la fonction sdf(), qui reçoit une position 2D dans l’espace en entrée. Les concepts que vous apprendrez ici se généraliseront directement à l’espace 3D et vous permettront de modéliser une scène 3D.

Commencez simplement – essayez d’abord d’utiliser simplement la composante x ou y du point p et observez le résultat:

float sdf(vec2 p){ return p.y;}

Le résultat devrait être le suivant:

Le vert désigne les surfaces « extérieures », le rouge désigne les surfaces « intérieures », la ligne blanche délimite la surface elle-même et l’ombrage dans les régions intérieur / extérieur illustre la distance iso-lignes – lignes à des distances fixes. En 2D, ce SDF modélise une ligne horizontale en 2D à y=0. Quelle sorte de primitive géométrique cela représenterait-il en 3D?

Une autre bonne chose à essayer est d’utiliser les distances, par exemple: return length(p);. Cet opérateur renvoie l’amplitude du vecteur, et dans ce cas, il nous donne la distance du point actuel à l’origine.

Un point n’est pas une chose très intéressante à rendre car un point est infinitésimal, et nos rayons le manqueraient toujours!Nous pouvons donner au point une surface en soustrayant le rayon souhaité de la distance: return length(p) - 0.25;.Nous pouvons également modifier le point d’entrée avant de prendre sa magnitude : length(p - vec2(0.0, 0.2)) - 0.25;.Quel effet cela a-t-il sur la forme?Quelles valeurs la fonction pourrait-elle renvoyer pour les points « à l’intérieur » du cercle?

Félicitations – vous venez de modéliser un cercle en mathématiques :). Cela s’étendra trivialement à la 3D, auquel cas il modélise une sphère. Contrastez cette représentation de scène avec d’autres représentations de scènes « explicites » telles que des mailles triangulaires ou des surfaces NURBS. Nous avons créé une sphère en quelques minutes avec une seule ligne de code, et notre code correspond directement à une définition mathématique pour une sphère – « l’ensemble de tous les points équidistants d’un point central ».

Pour d’autres types de primitives, les fonctions de distance sont tout aussi élégantes. iq a fait une excellente page de référence avec des images: http://iquilezles.org/www/articles/distfunctions/distfunctions.htm

Une fois que vous avez compris comment fonctionne une distance à une primitive – placez-la dans une boîte – définissez une fonction pour cela afin que vous n’ayez pas besoin de vous souvenir et d’écrire le code à chaque fois. Il existe une fonction déjà définie pour le cercle sdCircle() que vous pouvez trouver dans le shader. Ajoutez toutes les primitives que vous souhaitez.

Combiner des formes

Maintenant, nous savons comment créer des primitives individuelles, comment les combiner pour définir une scène avec plusieurs formes?

Une façon de le faire est l’opérateur « union » – qui est défini comme le minimum de deux distances. Il est préférable d’expérimenter avec le code afin de bien comprendre cela, mais l’intuition est que le SDF donne la distance à la surface la plus proche, et si la scène a plusieurs objets, vous voulez la distance à l’objet le plus proche, qui sera le minimum des distances à chaque objet.

Dans le code, cela peut ressembler à ce qui suit:

float sdf(vec2 p){ float d = 1000.0; d = min(d, sdCircle(p, vec2(-0.1, 0.4), 0.15)); d = min(d, sdCircle(p, vec2( 0.5, 0.1), 0.35)); return d;}

De cette façon, nous pouvons combiner de manière compacte de nombreuses formes. Une fois cela compris, il convient d’utiliser la fonction opU(), qui signifie « opération union ».

Cela ne fait que gratter la surface de ce qui est possible. Nous pouvons obtenir des mélanges lisses en utilisant une fonction min douce et sophistiquée – essayez d’utiliser le opBlend() fourni. Il existe de nombreuses autres techniques intéressantes qui peuvent être appliquées, le lecteur intéressé est renvoyé à cette introduction étendue à la construction de scènes avec SDF: https://www.youtube.com/watch?v=s8nFqwOho-s

Exemple:

Transition vers 3D

J’espère que vous avez acquis une compréhension de base de la façon dont les champs de distance peuvent être utilisés pour représenter les données de scène, et comment nous utiliserons raymarching pour trouver des points d’intersection avec la scène. Nous allons maintenant commencer à travailler en trois dimensions, où la vraie magie se produit.

Nous vous recommandons d’enregistrer votre shader actuel et d’en démarrer un nouveau afin que vous puissiez vous référer ultérieurement à votre visualisation 2D.La plupart des assistants peuvent copier dans votre nouveau shader et le faire fonctionner en 3D en échangeant les vec2 s avec vec3 s.

Boucle de marche des rayons

Plutôt que de visualiser le SDF comme nous l’avons fait en 2D, nous allons passer directement au rendu de la scène. Voici l’idée de base de la façon dont nous allons implémenter ray marching (en pseudo-code):

Main function Evaluate camera Call RenderRayRenderRay function Raymarch to find intersection of ray with scene Shade

Ces étapes vont maintenant être décrites chacune plus en détail.

Caméra

vec3 getCameraRayDir(vec2 uv, vec3 camPos, vec3 camTarget){ // Calculate camera's "orthonormal basis", i.e. its transform matrix components vec3 camForward = normalize(camTarget - camPos); vec3 camRight = normalize(cross(vec3(0.0, 1.0, 0.0), camForward)); vec3 camUp = normalize(cross(camForward, camRight)); float fPersp = 2.0; vec3 vDir = normalize(uv.x * camRight + uv.y * camUp + camForward * fPersp); return vDir;}

Cette fonction calcule d’abord les trois axes de la matrice de « vue » de la caméra; les vecteurs avant, droit et haut.Le vecteur avant est le vecteur normalisé de la position de la caméra à la position de la cible de regard.Le vecteur droit est trouvé en croisant le vecteur avant avec l’axe du monde vers le haut.Les vecteurs avant et droit sont ensuite croisés pour obtenir le vecteur caméra haut.

Enfin, le rayon de la caméra est calculé à l’aide de cette image en prenant un point devant la caméra et en le décalant dans les directions droite et haut de la caméra en utilisant les coordonnées de pixels uv.fPersp nous permet de contrôler indirectement le champ de vision de notre caméra. Vous pouvez considérer cette multiplication comme un déplacement du plan proche de plus en plus proche de la caméra. Expérimentez avec différentes valeurs pour voir le résultat.

Définition de scène

float sdSphere(vec3 p, float r){ return length(p) - r;} float sdf(vec3 pos){ float t = sdSphere(pos-vec3(0.0, 0.0, 10.0), 3.0); return t;}

Comme vous pouvez le voir, nous avons ajouté un sdSphere() qui est identique à sdCircle sauf pour le nombre de composants dans notre point d’entrée.

Raymarching

Pseudo code:

castRay for i in step count: sample scene if within threshold return dist return -1

Essayez d’écrire cela vous-même – si vous êtes coincé seulement, jetez un coup d’œil à la solution ci-dessous.

Code réel:

float castRay(vec3 rayOrigin, vec3 rayDir){ float t = 0.0; // Stores current distance along ray for (int i = 0; i < 64; i++) { float res = SDF(rayOrigin + rayDir * t); if (res < (0.0001*t)) { return t; } t += res; } return -1.0;}

Nous allons maintenant ajouter une fonction render, qui sera éventuellement responsable de l’ombrage du point d’intersection trouvé. Pour l’instant cependant, affichons la distance à la scène pour vérifier que nous sommes sur la bonne voie. Nous allons l’adapter et l’inverser pour mieux voir les différences.

vec3 render(vec3 rayOrigin, vec3 rayDir){ float t = castRay(rayOrigin, rayDir); // Visualize depth vec3 col = vec3(1.0-t*0.075); return col;}

Pour calculer la direction de chaque rayon, nous voulons transformer l’entrée de coordonnées de pixel fragCoord de la plage , , où w et h sont la largeur et la hauteur de l’écran en pixels, et a est le rapport d’aspect de l’écran. Nous pouvons ensuite passer la valeur renvoyée par cet assistant dans la fonction getCameraRayDir que nous avons définie ci-dessus pour obtenir la direction du rayon.

vec2 normalizeScreenCoords(vec2 screenCoord){ vec2 result = 2.0 * (screenCoord/iResolution.xy - 0.5); result.x *= iResolution.x/iResolution.y; // Correct for aspect ratio return result;}

Notre fonction d’image principale se présente alors comme suit:

void mainImage(out vec4 fragColor, vec2 fragCoord){ vec3 camPos = vec3(0, 0, -1); vec3 camTarget = vec3(0, 0, 0); vec2 uv = normalizeScreenCoords(fragCoord); vec3 rayDir = getCameraRayDir(uv, camPos, camTarget); vec3 col = render(camPos, rayDir); fragColor = vec4(col, 1); // Output to screen}

Exercices:

  • Expérimentez avec le nombre de pas et observez comment le résultat change.
  • Expérimentez avec le seuil de terminaison et observez comment les résultats changent.

Pour le programme de travail complet, voir Shadertoy: Partie 1a

Terme ambiant

Pour obtenir de la couleur dans la scène, nous allons d’abord différencier les objets de l’arrière-plan.

Pour ce faire, nous pouvons renvoyer -1 dans castRay pour signaler que rien n’a été touché. Nous pouvons ensuite gérer ce cas dans render.

vec3 render(vec3 rayOrigin, vec3 rayDir){ vec3 col; float t = castRay(rayOrigin, rayDir); if (t == -1.0) { // Skybox colour col = vec3(0.30, 0.36, 0.60) - (rayDir.y * 0.7); } else { vec3 objectSurfaceColour = vec3(0.4, 0.8, 0.1); vec3 ambient = vec3(0.02, 0.021, 0.02); col = ambient * objectSurfaceColour; } return col;}

https://www.shadertoy.com/view/4tdBzj

Terme diffus

Pour obtenir un éclairage plus réaliste, calculons la surface normale afin de pouvoir calculer l’éclairage lambertien de base.

Pour calculer la normale, nous allons calculer le gradient de la surface dans les trois axes.

Ce que cela signifie en pratique, c’est d’échantillonner le SDF quatre fois supplémentaires, chacune légèrement décalée de notre rayon primaire.

vec3 calcNormal(vec3 pos){ // Center sample float c = sdf(pos); // Use offset samples to compute gradient / normal vec2 eps_zero = vec2(0.001, 0.0); return normalize(vec3( sdf(pos + eps_zero.xyy), sdf(pos + eps_zero.yxy), sdf(pos + eps_zero.yyx) ) - c);}

Une excellente façon d’inspecter les normales est de les afficher comme si elles représentaient la couleur. Voici à quoi devrait ressembler une sphère lors de l’affichage de sa normale mise à l’échelle et biaisée (ramenée de à car votre moniteur ne peut pas afficher de valeurs de couleur négatives)

col = N * vec3(0.5) + vec3(0.5);

Maintenant que nous avons une normale, nous pouvons prendre le produit scalaire entre elle et la direction de la lumière.

Cela nous indiquera à quel point la surface fait directement face à la lumière et donc à quel point elle devrait être lumineuse.

Nous prenons le maximum de cette valeur avec 0 pour empêcher les valeurs négatives de donner des effets indésirables sur le côté obscur des objets.

// L is vector from surface point to light, N is surface normal. N and L must be normalized!float NoL = max(dot(N, L), 0.0);vec3 LDirectional = vec3(0.9, 0.9, 0.8) * NoL;vec3 LAmbient = vec3(0.03, 0.04, 0.1);vec3 diffuse = col * (LDirectional + LAmbient);

Une partie très importante du rendu qui peut facilement être négligée est la correction gamma. Les valeurs de pixels envoyées au moniteur sont dans l’espace gamma, qui est un espace non linéaire utilisé pour maximiser la précision, en utilisant moins de bits dans des plages d’intensité auxquelles les humains sont moins sensibles.

Comme les moniteurs ne fonctionnent pas dans un espace « linéaire », nous devons compenser leur courbe gamma avant de produire une couleur. La différence est très perceptible et doit toujours être corrigée. En réalité, nous ne savons pas que la courbe gamma pour un dispositif d’affichage particulier est, donc toute la situation avec la technologie d’affichage est un gâchis terrible (d’où l’étape de réglage du gamma dans de nombreux jeux), mais une hypothèse commune est la courbe gamma suivante:

La constante 0,4545 est simplement 1.0 / 2.2

col = pow(col, vec3(0.4545)); // Gamma correction

https://www.shadertoy.com/view/4t3fzn

Ombres

Pour calculer les ombres, nous pouvons lancer un rayon commençant au point où nous avons croisé la scène et allant dans la direction de la source lumineuse.

Si cette marche de rayon nous amène à frapper quelque chose, alors nous savons que la lumière sera également obstruée et que ce pixel est donc dans l’ombre.

float shadow = 0.0;vec3 shadowRayOrigin = pos + N * 0.01;vec3 shadowRayDir = L;IntersectionResult shadowRayIntersection = castRay(shadowRayOrigin, shadowRayDir);if (shadowRayIntersection.mat != -1.0){ shadow = 1.0;}col = mix(col, col*0.2, shadow);

Plan de masse

Ajoutons un plan de masse pour que nous puissions mieux voir les ombres projetées par nos sphères.

La composante w de n représente la distance du plan par rapport à l’origine.

float sdPlane(vec3 p, vec4 n){ return dot(p, n.xyz) + n.w;}

Ombres douces

Les ombres dans la vraie vie ne s’arrêtent pas immédiatement, elles ont une certaine retombée, appelée pénombre.

Nous pouvons modéliser cela en prenant plusieurs rayons de notre point de surface, chacun avec des directions légèrement différentes.

Nous pouvons ensuite additionner le résultat et la moyenne sur le nombre d’itérations que nous avons effectuées. Cela fera que les bords de l’ombre auront

certains rayons frappent, et d’autres manquent, donnant une obscurité de 50%.

Trouver un nombre pseudo-aléatoire peut être fait de plusieurs façons, nous utiliserons cependant ce qui suit:

float rand(vec2 co){ return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);}

Cette fonction renverra un nombre dans la plage [0, 1). Nous savons que la sortie est liée à cette plage car l’opération la plus externe est la fracturation, qui renvoie la composante fractionnaire d’un nombre à virgule flottante.

Nous pouvons ensuite l’utiliser pour calculer notre rayon d’ombre comme suit:

float shadow = 0.0;float shadowRayCount = 1.0;for (float s = 0.0; s < shadowRayCount; s++){ vec3 shadowRayOrigin = pos + N * 0.01; float r = rand(vec2(rayDir.xy)) * 2.0 - 1.0; vec3 shadowRayDir = L + vec3(1.0 * SHADOW_FALLOFF) * r; IntersectionResult shadowRayIntersection = castRay(shadowRayOrigin, shadowRayDir); if (shadowRayIntersection.mat != -1.0) { shadow += 1.0; }}col = mix(col, col*0.2, shadow/shadowRayCount);

Mappage de texture

Plutôt que de définir une couleur de surface unique (ou une autre caractéristique) uniformément sur toute la surface, on peut définir des motifs à appliquer à la surface à l’aide de textures.Nous couvrirons trois façons d’y parvenir.

Mappage de texture 3D

Il existe des textures de volume facilement accessibles dans shadertoy qui peuvent être affectées à un canal. Essayez d’échantillonner l’une de ces textures en utilisant la position 3D du point de surface:

// assign a 3D noise texture to iChannel0 and then sample based on world positionfloat textureFreq = 0.5;vec3 surfaceCol = texture(iChannel0, textureFreq * surfacePos).xyz;

Une façon d’échantillonner le bruit consiste à additionner plusieurs échelles, en utilisant quelque chose comme ceci:

// assign a 3D noise texture to iChannel0 and then sample based on world positionfloat textureFreq = 0.5;vec3 surfaceCol = 0.5 * texture(iChannel0, 1.0 * textureFreq * surfacePos).xyz + 0.25 * texture(iChannel0, 2.0 * textureFreq * surfacePos).xyz + 0.125 * texture(iChannel0, 4.0 * textureFreq * surfacePos).xyz + 0.0625 * texture(iChannel0, 8.0 * textureFreq * surfacePos).xyz ;

Les constantes / poids ci-dessus sont généralement utilisées pour un bruit fractal, mais elles peuvent prendre toutes les valeurs souhaitées. Essayez d’expérimenter avec des poids / échelles / couleurs et de voir quels effets intéressants vous pouvez obtenir.

Essayez d’animer votre objet à l’aide d’iTime et observez le comportement de la texture du volume. Ce comportement peut-il être modifié ?

Mappage de texture 2D

Appliquer une texture 2D est un problème intéressant – comment projeter la texture sur la surface? Dans les graphiques 3D normaux, chaque triangle d’un objet a un ou plusieurs UV qui fournissent les coordonnées de la région de texture qui doit être mappée au triangle (mappage de texture). Dans notre cas, nous n’avons pas de UV fournis, nous devons donc trouver comment échantillonner la texture.

Une approche consiste à échantillonner la texture à l’aide d’une projection du monde de haut en bas, en échantillonnant la texture en fonction des coordonnées X & Z:

// top down projectionfloat textureFreq = 0.5;vec2 uv = textureFreq * surfacePos.xz; // sample texturevec3 surfaceCol = texture2D(iChannel0, uv).xyz;

Quelles limites voyez-vous avec cette approche?

Mappage triplanaire

Un moyen plus avancé de mapper les textures consiste à effectuer 3 projections à partir des axes principaux, puis à mélanger le résultat à l’aide du mappage triplanaire. Le but du mélange est de choisir la meilleure texture pour chaque point de la surface. Une possibilité est de définir les poids de mélange en fonction de l’alignement de la surface normale avec chaque axe du monde. Une surface tournée vers l’avant avec l’un des axes recevra un poids de mélange important:

vec3 triplanarMap(vec3 surfacePos, vec3 normal){ // Take projections along 3 axes, sample texture values from each projection, and stack into a matrix mat3 triMapSamples = mat3( texture(iChannel0, surfacePos.yz).rgb, texture(iChannel0, surfacePos.xz).rgb, texture(iChannel0, surfacePos.xy).rgb ); // Weight three samples by absolute value of normal components return triMapSamples * abs(normal);}

Quelles limites voyez-vous avec cette approche?

Matériaux

En plus de la distance que nous revenons de la fonction de castRay, nous pouvons également renvoyer un index qui représente le matériau de l’objet touché. Nous pouvons utiliser cet index pour colorer les objets en conséquence.

Nos opérateurs devront prendre des vec2 plutôt que des flottants et comparer le premier composant de chacun.

Maintenant, lors de la définition de notre scène, nous allons également spécifier un matériau pour chaque primitive en tant que composant y d’un vec2:

vec2 res = vec2(sdSphere(pos-vec3(3,-2.5,10), 2.5), 0.1);res = opU(res, vec2(sdSphere(pos-vec3(-3, -2.5, 10), 2.5), 2.0));res = opU(res, vec2(sdSphere(pos-vec3(0, 2.5, 10), 2.5), 5.0));return res;

Nous pouvons ensuite multiplier cet indice de matériau par certaines valeurs dans la fonction de rendu pour obtenir des couleurs différentes pour chaque objet. Essayez différentes valeurs.

col = vec3(0.18*m, 0.6-0.05*m, 0.2)if (m == 2.0){ col *= triplanarMap(pos, N, 0.6);}

Colorions le plan de masse en utilisant un motif en damier. J’ai pris cette fonction de checkerbox analytiquement anti-alias de fantaisie du site Web d’Inigo Quilez.

float checkers(vec2 p){ vec2 w = fwidth(p) + 0.001; vec2 i = 2.0*(abs(fract((p-0.5*w)*0.5)-0.5)-abs(fract((p+0.5*w)*0.5)-0.5))/w; return 0.5 - 0.5*i.x*i.y;}

Nous passerons dans les composants xz de notre position de plan pour que le motif se répète dans ces dimensions.

https://www.shadertoy.com/view/Xl3fzn

Brouillard

Nous pouvons maintenant ajouter du brouillard à la scène en fonction de la distance à laquelle chaque intersection s’est produite par rapport à la caméra.

Voyez si vous pouvez obtenir quelque chose de similaire à ce qui suit:

https://www.shadertoy.com/view/Xtcfzn

Forme & mélange de matériaupour éviter le pli dur donné par l’opérateur min, nous pouvons utiliser un opérateur plus sophistiqué qui mélange les formes en douceur.

// polynomial smooth min (k = 0.1);float sminCubic(float a, float b, float k){ float h = max(k-abs(a-b), 0.0); return min(a, b) - h*h*h/(6.0*k*k);} vec2 opBlend(vec2 d1, vec2 d2){ float k = 2.0; float d = sminCubic(d1.x, d2.x, k); float m = mix(d1.y, d2.y, clamp(d1.x-d,0.0,1.0)); return vec2(d, m);}

Anti-aliasing

En échantillonnant la scène plusieurs fois avec des vecteurs de direction de caméra légèrement décalés, nous pouvons obtenir une valeur lissée qui évite l’aliasing.

J’ai mis en évidence le calcul de la couleur de la scène à sa propre fonction pour le rendre plus clair dans la boucle.

float AA_size = 2.0;float count = 0.0;for (float aaY = 0.0; aaY < AA_size; aaY++){ for (float aaX = 0.0; aaX < AA_size; aaX++) { fragColor += getSceneColor(fragCoord + vec2(aaX, aaY) / AA_size); count += 1.0; }}fragColor /= count;

Optimisation du nombre de pas

Si nous visualisons le nombre de pas que nous prenons pour chaque pixel en rouge, nous pouvons clairement voir que les rayons qui ne frappent rien sont responsables de la plupart de nos itérations.

Cela peut augmenter considérablement les performances de certaines scènes.

if (t > drawDist) return backgroundColor;

Forme & interpolation des matériaux

Nous pouvons interpoler entre deux formes en utilisant la fonction mix et en utilisant iTime pour moduler dans le temps.

vec2 shapeA = vec2(sdBox(pos-vec3(6.5, -3.0, 8), vec3(1.5)), 1.5);vec2 shapeB = vec2(sdSphere(pos-vec3(6.5, -3.0, 8), 1.5), 3.0);res = opU(res, mix(shapeA, shapeB, sin(iTime)*0.5+0.5));

Répétition de domaine

Il est assez facile de répéter une forme en utilisant un champ de distance signé, il vous suffit essentiellement de modulo la position d’entrée dans une ou plusieurs dimensions.

Cette technique peut être utilisée par exemple pour répéter plusieurs fois une colonne sans augmenter la taille de représentation de la scène.

Ici, j’ai répété les trois composantes de la position d’entrée, puis j’ai utilisé l’opérateur de soustraction (max()) pour limiter la répétition à une boîte englobante.

Un problème est que vous devez soustraire la moitié de la valeur que vous modulez afin de centrer la répétition sur votre forme pour ne pas la couper en deux.

float repeat(float d, float domain){ return mod(d, domain)-domain/2.0;}

Effets de post-traitement

Vignette

En assombrissant les pixels qui sont plus éloignés du centre de l’écran, nous pouvons obtenir un effet de vignette simple.

Contraste

Des valeurs plus sombres et plus claires peuvent être accentuées, ce qui entraîne une augmentation de la plage dynamique perçue avec l’intensité de l’image.

col = smoothstep(0.0,1.0,col);

 » Occlusion ambiante »

Si nous prenons l’inverse de l’image ci-dessus (dans les optimisations), nous pouvons obtenir un effet étrange de type AO.

col *= (1.0-vec3(steps/maxSteps));

Ad infinitum

Comme vous pouvez le voir, de nombreux effets de post-traitement peuvent être implémentés de manière triviale; jouez avec différentes fonctions et voyez quels autres effets vous pouvez créer.

www.shadertoy.com/view/MtdBzs

Et après ?

Nous venons de couvrir les bases ici; il y a beaucoup plus à explorer dans ce domaine, comme:

  • Diffusion souterraine
  • Occlusion ambiante
  • Primitives animées
  • Fonctions de déformation primitives (torsion, courbure, …)
  • Transparence (réfraction, caustique, …)
  • Optimisations (hiérarchies de volumes limites)

Parcourez ShaderToy pour vous inspirer de ce qui peut être fait et parcourez divers shaders pour voir comment les différents effets sont implémentés. De nombreux shaders ont des variables que vous pouvez modifier et voir instantanément les effets de (alt-enter est le raccourci pour compiler!).

Donnez également une lecture des références si vous souhaitez en savoir plus!

Merci d’avoir lu! N’oubliez pas de nous envoyer vos shaders sympas! Si vous avez des commentaires sur le cours, nous aimerions également les entendre!

Contactez-nous sur twitter @liqwidice & @hdb1

Electric Square recrute!

Lecture recommandée:

Fonctions SDF: http://jamie-wong.com/2016/07/15/ray-marching-signed-distance-functions/

Démo Claybook: https://www.youtube.com/watch?v=Xpf7Ua3UqOA

Ray Tracing en un Week-end: http://in1weekend.blogspot.com/2016/01/ray-tracing-in-one-weekend.html

Bible de rendu physique, PBRT: https://www.pbrt.org/

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.