taller de iluminación / raymarching eléctrico

Presentado por Electric Square

Creado y presentado por AJ Weeks & Huw Bowles

Visión general

La representación de una imagen implica determinar el color de cada píxel de la imagen, lo que requiere averiguar qué superficie se encuentra detrás del píxel en el mundo, y luego ‘sombrearla’ para calcular un color final.

Las GPU de la generación actual toman mallas triangulares como entrada, las rasterizan en píxeles (llamados fragmentos antes de que se dibujen en una pantalla) y luego las sombrean para calcular su contribución a la imagen. Si bien esta canalización es actualmente omnipresente, también es complicada y no necesariamente la mejor manera de aprender gráficos.

Un enfoque alternativo es proyectar un rayo a través de cada píxel y cruzarlo con las superficies de la escena, y luego calcular el sombreado.

Este curso presenta una técnica para el raycasting a través de «campos a distancia». Un campo de distancia es una función que devuelve qué tan cerca está un punto determinado de la superficie más cercana de la escena. Esta distancia define el radio de una esfera de espacio vacío alrededor de cada punto. Los campos de distancia con signo (SDF) son campos de distancia definidos tanto dentro como fuera de objetos; si la posición consultada está ‘dentro’ de una superficie, su distancia se reportará como negativa, de lo contrario será positiva.

¿Qué es posible con ray marching?

El juego ‘Claybook’ solo usa campos de distancia para representar la escena. Esto le ofrece muchas posibilidades interesantes, como topologías de superficie completamente dinámicas y morfología de formas. Estos efectos serían muy difíciles de lograr con mallas triangulares. Otros beneficios incluyen sombras suaves fáciles de implementar y de alta calidad y oclusión ambiental.

https://www.claybookgame.com/

La siguiente imagen también se renderizó en tiempo real utilizando las técnicas que cubriremos hoy (además de muchas técnicas sofisticadas en las que no tendremos tiempo de sumergirnos).

Puede ejecutarlo en vivo en su navegador aquí: https://www.shadertoy.com/view/ld3Gz2

Al usar un SDF (campo de distancia con signo), la geometría de esta escena no tuvo que crearse en un DCC como Maya, sino que se representa completamente paramétricamente. Esto hace que sea trivial animar la forma simplemente variando las entradas a la función de asignación de escenas.

Otros efectos gráficos se simplifican mediante raymarching en comparación con las alternativas de rasterización tradicionales. La dispersión del subsuelo, por ejemplo, requiere simplemente enviar unos cuantos rayos adicionales a la superficie para ver qué tan gruesa es. La oclusión ambiental, el suavizado y la profundidad de campo son otras tres técnicas que requieren solo unas pocas líneas adicionales y, sin embargo, mejoran en gran medida la calidad de la imagen.

Campos de distancia Raymarching

Marcharemos a lo largo de cada rayo y buscaremos una intersección con una superficie en la escena. Una forma de hacerlo sería comenzar en el origen del rayo (en el plano de la cámara) y tomar pasos uniformes a lo largo del rayo, evaluando el campo de distancia en cada punto. Cuando la distancia a la escena es inferior a un valor de umbral, sabemos que hemos alcanzado una superficie y, por lo tanto, podemos terminar el raymarch y sombrear ese píxel.

Un enfoque más eficiente es utilizar la distancia devuelta por el SDF para determinar el tamaño del siguiente paso. Como se mencionó anteriormente, la distancia devuelta por un SDF puede considerarse como el radio de una esfera de espacio vacío alrededor del punto de entrada. Por lo tanto, es seguro caminar por esta cantidad a lo largo del rayo porque sabemos que no pasaremos a través de ninguna superficie.

En la siguiente representación en 2D de raymarching, el centro de cada círculo es desde donde se muestreó la escena. El rayo se marchó a lo largo de esa distancia (extendiéndose hasta el radio del círculo), y luego se volvió a muestrear.

Como puede ver, el muestreo del SDF no le da el punto de intersección exacto de su rayo, sino una distancia mínima que puede recorrer sin pasar por una superficie.

Una vez que esta distancia está por debajo de un cierto umbral, el raymarch termina el píxel se puede sombrear en función de las propiedades de la superficie con la que se cruza.

Juega con este sombreador en tu navegador aquí: (haz clic y arrastra la imagen para establecer la dirección del rayo) https://www.shadertoy.com/view/lslXD8

Comparación con el trazado de rayos

En este punto, uno podría preguntarse por qué no calculamos la intersección con la escena directamente utilizando matemáticas analíticas, utilizando una técnica conocida como Trazado de rayos. Así es como funcionarían normalmente los renderizados fuera de línea: todos los triángulos de la escena están indexados en algún tipo de estructura de datos espaciales, como una Jerarquía de Volumen Delimitador (BVH) o un árbol kD, que permiten la intersección eficiente de triángulos situados a lo largo de un rayo.

Nos raymarch campos de distancia en su lugar porque:

  • Es muy simple implementar la rutina de fundición de rayos
  • Evitamos toda la complejidad de implementar intersecciones de triángulos de rayos y estructuras de datos BVH
  • No necesitamos crear la representación explícita de escenas: mallas triangulares, coordenadas tex, colores, etc
  • Nos beneficiamos de una gama de características útiles de campos de distancia, algunas de las cuales se mencionan anteriormente

Dicho lo anterior, hay algunos puntos de entrada elegantes/simples en el trazado de rayos. El libro gratuito Trazado de rayos en un fin de semana (y los capítulos posteriores) son muy recomendables y son una lectura esencial para cualquier persona interesada en los gráficos.

¡Comencemos!

ShaderToy

ShaderToy es un sitio web de creación de sombreadores y una plataforma para navegar, compartir y discutir sobre sombreadores.

Si bien puedes entrar directamente y comenzar a escribir un nuevo sombreador sin crear una cuenta, esto es peligroso, ya que puedes perder trabajo fácilmente si hay problemas de conexión o si cuelgas la GPU (lo haces fácilmente, por ejemplo, creando un bucle infinito).Por lo tanto, recomendamos encarecidamente crear una cuenta (es rápido/fácil/gratis) dirigiéndose aquí: https://www.shadertoy.com/signin y guardando regularmente.

Para obtener una descripción general de ShaderToy y una guía de introducción, recomendamos seguir un tutorial como este de @The_ArtOfCode: https://www.youtube.com/watch?v=u5HAYVHsasc. Lo básico aquí es necesario para seguir el resto del taller.

2D SDF demo

Proporcionamos un marco simple para definir y visualizar campos de distancia firmados en 2D.

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

Antes de definir el campo de distancia el resultado será completamente en blanco. El objetivo de esta sección es diseñar un SDF que dé la forma de escena deseada (contorno blanco). En el código, esta distancia es calculada por la función sdf(), a la que se le da una posición 2D en el espacio como entrada. Los conceptos que aprendas aquí se generalizarán directamente al espacio 3D y te permitirán modelar una escena 3D.

Inicio simple: intente primero usar el componente x o y del punto p y observar el resultado:

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

El resultado debe verse como sigue:

El verde denota superficies ‘exteriores’, el rojo denota superficies ‘interiores’, la línea blanca delinea la superficie en sí, y el sombreado en las regiones interior/exterior ilustra la distancia iso-líneas – líneas a distancias fijas. En 2D, este SDF modela una línea horizontal en 2D a y=0. ¿Qué tipo de primitiva geométrica representaría esto en 3D?

Otra buena opción es usar distancias, por ejemplo: return length(p);. Este operador devuelve la magnitud del vector, y en este caso nos da la distancia del punto actual al origen.

Un punto no es una cosa muy interesante para representar como un punto infinitesimal, y nuestro rayos siempre te lo pierdas!Podemos dar al punto un área restando el radio deseado de la distancia: return length(p) - 0.25;.También podemos modificar el punto de entrada antes de tomar su magnitud: length(p - vec2(0.0, 0.2)) - 0.25;.¿Qué efecto tiene esto en la forma?¿Qué valores podría devolver la función para los puntos ‘dentro’ del círculo?

Felicitaciones – acabas de modelar un círculo usando matemáticas :). Esto se extenderá trivialmente al 3D, en cuyo caso modela una esfera. Contraste esta representación de escena con otras representaciones de escenas «explícitas», como mallas triangulares o superficies NURBS. Creamos una esfera en minutos con una sola línea de código, y nuestro código se asigna directamente a una definición matemática para una esfera: «el conjunto de todos los puntos equidistantes de un punto central».

Para otros tipos de primitivas, las funciones de distancia son igualmente elegantes. iq hizo una gran página de referencia con imágenes: http://iquilezles.org/www/articles/distfunctions/distfunctions.htm

Una vez que entienda cómo funciona una distancia a una primitiva, póngala en una caja, defina una función para ella para que no necesite recordar y escribir el código cada vez. Hay una función ya definida para el círculo sdCircle() que puedes encontrar en el sombreado. Añade las primitivas que desees.

Combinar formas

Ahora sabemos cómo crear primitivas individuales, ¿cómo podemos combinarlas para definir una escena con múltiples formas?

Una forma de hacer esto es el operador ‘union’, que se define como el mínimo de dos distancias. Es mejor experimentar con el código para obtener una comprensión sólida de esto, pero la intuición es que el SDF da la distancia a la superficie más cercana, y si la escena tiene varios objetos, desea la distancia al objeto más cercano, que será la distancia mínima a cada objeto.

En el código esto puede verse como sigue:

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 esta manera podemos combinar de forma compacta muchas formas. Una vez entendido esto, se debe usar la función opU(), que significa ‘operación unión’.

Esto es solo arañar la superficie de lo que es posible. Podemos obtener mezclas suaves utilizando una función de min suave de lujo: intente usar el opBlend() proporcionado. Hay muchas otras técnicas interesantes que se pueden aplicar, el lector interesado se refiere a esta introducción extendida a la construcción de escenas con SDFs: https://www.youtube.com/watch?v=s8nFqwOho-s

Ejemplo:

Transición a 3D

Esperamos que haya adquirido una comprensión básica de cómo se pueden usar los campos de distancia para representar los datos de la escena y cómo usaremos raymarching para encontrar puntos de intersección con la escena. Ahora vamos a empezar a trabajar en tres dimensiones, donde ocurre la verdadera magia.

Recomendamos guardar su sombreador actual e iniciar uno nuevo para que pueda volver a consultar su visualización 2D más adelante.La mayoría de los ayudantes pueden copiarse en su nuevo sombreador y funcionar en 3D intercambiando vec2s con vec3s.

Loop de marcha de rayos

En lugar de visualizar el SDF como hicimos en 2D, vamos a pasar directamente a renderizar la escena. Esta es la idea básica de cómo implementaremos ray marching (en pseudo código):

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

Estos pasos ahora se describirán con más detalle.

Cámara

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

Esta función calcula primero los tres ejes de la matriz de ‘vista’ de la cámara; los vectores hacia adelante, hacia la derecha y hacia arriba.El vector de avance es el vector normalizado desde la posición de la cámara hasta la posición de objetivo del look.El vector derecho se encuentra cruzando el vector hacia adelante con el eje del mundo hacia arriba.Los vectores hacia adelante y hacia la derecha se cruzan para obtener el vector hacia arriba de la cámara.

Finalmente, el rayo de la cámara se calcula utilizando este fotograma tomando un punto frente a la cámara y compensándolo en las direcciones derecha y hacia arriba de la cámara utilizando las coordenadas de píxel uv.fPersp nos permite controlar indirectamente el campo de visión de nuestra cámara. Puedes pensar en esta multiplicación como mover el plano cercano más cerca y más lejos de la cámara. Experimente con diferentes valores para ver el resultado.

Definición de escena

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

Como puede ver, hemos agregado un sdSphere() que es idéntico a sdCircle, excepto por el número de componentes en nuestro punto de entrada.

Raymarching

Pseudo código:

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

Intente escribir esto usted mismo, si se queda atascado, eche un vistazo a la solución a continuación.

Código real:

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

Ahora agregaremos una función render, que eventualmente será responsable de sombrear el punto de intersección encontrado. Por ahora, sin embargo, vamos a mostrar la distancia a la escena para comprobar que estamos en camino. Lo escalaremos e invertiremos para ver mejor las diferencias.

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

Para calcular la dirección de cada rayo, desearemos transformar la entrada de coordenadas de píxel fragCoord del rango , , donde w y h son el ancho y alto de la pantalla en píxeles, y a es la relación de aspecto de la pantalla. Luego podemos pasar el valor devuelto por este ayudante a la función getCameraRayDir que definimos anteriormente para obtener la dirección del rayo.

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

Nuestra función de imagen principal se ve de la siguiente manera:

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}

Ejercicios:

  • Experimente con el recuento de pasos y observe cómo cambian los resultados.
  • Experimente con el umbral de terminación y observe cómo cambian los resultados.

Para un programa de trabajo completo, consulte Shadertoy: Parte 1a

Término ambiental

Para obtener un poco de color en la escena, primero vamos a diferenciar entre los objetos y el fondo.

Para hacer esto, podemos devolver -1 en castRay para indicar que no se ha alcanzado nada. Luego podemos manejar ese caso en 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

Término difuso

Para obtener una iluminación más realista, calculemos la superficie normal para poder calcular la iluminación lambertiana básica.

Para calcular lo normal, vamos a calcular el gradiente de la superficie en los tres ejes.

Lo que esto significa en la práctica es muestrear el SDF cuatro veces más, cada una ligeramente desplazada de nuestro rayo primario.

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

Una gran manera de inspeccionar los normales es mostrándolos como si representaran color. Así es como debe verse una esfera al mostrar su normal sesgado y escalado (llevado de a , ya que su monitor no puede mostrar valores de color negativos)

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

Ahora que tenemos una normal, podemos tomar el producto escalar entre ella y la dirección de la luz.

Esto nos dirá cuán directamente la superficie está orientada a la luz y, por lo tanto, cuán brillante debe ser.

Tomamos el máximo de este valor con 0 para evitar que los valores negativos produzcan efectos no deseados en el lado oscuro de los objetos.

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

Una parte muy importante del renderizado que se puede pasar por alto fácilmente es la corrección gamma. Los valores de píxeles enviados al monitor están en el espacio gamma, que es un espacio no lineal utilizado para maximizar la precisión, al usar menos bits en rangos de intensidad a los que los humanos son menos sensibles.

Debido a que los monitores no funcionan en un espacio «lineal», necesitamos compensar su curva gamma antes de emitir un color. La diferencia es muy notable y siempre debe corregirse. En realidad, no sabemos la curva gamma de un dispositivo de visualización en particular, por lo que toda la situación con la tecnología de visualización es un desastre terrible (de ahí el paso de ajuste gamma en muchos juegos), pero una suposición común es la siguiente curva gamma:

La constante 0.4545 es simplemente 1.0 / 2.2

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

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

Sombras

Para calcular las sombras, podemos disparar un rayo comenzando en el punto en el que cruzamos la escena y yendo en la dirección de la fuente de luz.

Si esta marcha de rayos nos hace golpear algo, entonces sabemos que la luz también se obstruirá y, por lo tanto, este píxel está en sombra.

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

Plano de tierra

Agreguemos un plano de tierra para que podamos ver mejor las sombras proyectadas por nuestras esferas.

El componente w de n representa la distancia que el plano está desde el origen.

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

Sombras suaves

Las sombras en la vida real no se detienen de inmediato, tienen cierta caída, conocida como penumbra.

Podemos modelar esto tomando varios rayos de marcha desde nuestro punto de superficie, cada uno con direcciones ligeramente diferentes.

Luego podemos sumar el resultado y el promedio sobre el número de iteraciones que hicimos. Esto hará que los bordes de la sombra tengan

algunos rayos golpeen y otros fallen, dando un 50% de oscuridad.

Encontrar un número pseudo aleatorio se puede hacer de varias maneras, usaremos lo siguiente:

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

Esta función devolverá un número en el rango [0, 1). Sabemos que la salida está vinculada a este rango porque la operación más externa es fract, que devuelve el componente fraccionario de un número de coma flotante.

Luego podemos usar esto para calcular nuestro rayo de sombra de la siguiente manera:

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

Mapeo de texturas

En lugar de definir un solo color de superficie (u otra característica) de manera uniforme sobre toda la superficie, se pueden definir patrones para aplicar a la superficie utilizando texturas.Cubriremos tres formas de lograrlo.

Mapeo de texturas 3D

Hay texturas de volumen fácilmente accesibles en shadertoy que se pueden asignar a un canal. Pruebe a muestrear una de estas texturas utilizando la posición 3D del punto de superficie:

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

Una forma de muestrear el ruido es sumar varias escalas, utilizando algo como lo siguiente:

// 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 ;

Las constantes/pesos anteriores se usan típicamente para un ruido fractal, pero pueden tomar cualquier valor deseado. Prueba a experimentar con pesos / escalas / colores y ver qué efectos interesantes puedes lograr.

Intente animar su objeto usando ime y observe cómo se comporta la textura de volumen. ¿Se puede cambiar este comportamiento?

Mapeo de texturas 2D

Aplicar una textura 2D es un problema interesante: ¿cómo proyectar la textura sobre la superficie? En gráficos 3D normales, cada triángulo de un objeto tiene asignado uno o más UV que proporcionan las coordenadas de la región de textura que debería mapear al triángulo (mapeo de texturas). En nuestro caso, no tenemos UVs proporcionados, por lo que necesitamos averiguar cómo muestrear la textura.

Un enfoque es muestrear la textura usando una proyección de mundo de arriba hacia abajo, muestreando la textura basada en coordenadas X & Z:

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

¿Qué limitaciones ve con este enfoque?

Mapeo triplanar

Una forma más avanzada de mapear texturas es hacer 3 proyecciones desde los ejes primarios y, a continuación, mezclar el resultado utilizando el mapeo triplanar. El objetivo de la mezcla es elegir la mejor textura para cada punto de la superficie. Una posibilidad es definir los pesos de mezcla en función de la alineación de la superficie normal con cada eje del mundo. Una superficie que mira hacia el frente con uno de los ejes recibirá un gran peso de mezcla:

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

¿Qué limitaciones ve con este enfoque?

Materials

Junto con la distancia que regresamos de la función castRay, también podemos devolver un índice que representa el material del objeto golpeado. Podemos usar este índice para colorear objetos en consecuencia.

Nuestros operadores tendrán que tomar vec2 en lugar de flotadores, y comparar el primer componente de cada uno.

Ahora, al definir nuestra escena, también especificaremos un material para cada primitiva como componente y de 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;

Luego podemos multiplicar este índice de material por algunos valores en la función de renderizado para obtener diferentes colores para cada objeto. Prueba diferentes valores.

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

Vamos a colorear el plano de tierra usando un patrón de tablero de ajedrez. He tomado esta elegante función de casilla de verificación con anti-alias analítico del sitio web de Íñigo 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;}

Pasaremos los componentes xz de nuestra posición plana para que el patrón se repita en esas dimensiones.

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

Niebla

Ahora podemos añadir niebla a la escena en función de qué tan lejos se produjo cada intersección de la cámara.

Vea si puede obtener algo similar a lo siguiente:

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

Forma & mezcla de materialpara evitar el pliegue áspero dado por el operador mínimo, podemos usar un operador más sofisticado que mezcla las formas suavemente.

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

Suavizado

Al muestrear la escena muchas veces con vectores de dirección de cámara ligeramente compensados, podemos obtener un valor suavizado que evita el suavizado.

He sacado el cálculo del color de la escena a su propia función para que la llamada en el bucle sea más clara.

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;

Optimización de recuento de pasos

Si visualizamos cuántos pasos tomamos para cada píxel en rojo, podemos ver claramente que los rayos que no llegan a nada son responsables de la mayoría de nuestras iteraciones.

Esto puede dar un aumento significativo del rendimiento para ciertas escenas.

if (t > drawDist) return backgroundColor;

Forma & interpolación de material

Podemos interpolar entre dos formas usando la función mix y usando ime para modular en el tiempo.

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

Repetición de dominio

Es bastante fácil repetir una forma usando un campo de distancia con signo, esencialmente solo tiene que modular la posición de entrada en una o más dimensiones.

Esta técnica se puede utilizar, por ejemplo, para repetir una columna varias veces sin aumentar el tamaño de representación de la escena.

Aquí he repetido los tres componentes de la posición de entrada, luego he utilizado el operador de resta ( max() ) para limitar la repetición a un cuadro delimitador.

Una idea es que necesitas restar la mitad del valor por el que estás modulando para centrar la repetición en tu forma y no cortarla por la mitad.

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

Efectos de posprocesamiento

Viñeta

Oscureciendo los píxeles que están más lejos del centro de la pantalla, podemos obtener un simple efecto de viñeta.

Contraste

Se pueden acentuar los valores más oscuros y claros, lo que hace que el rango dinámico percibido aumente junto con la intensidad de la imagen.

col = smoothstep(0.0,1.0,col);

«Oclusión ambiental»

Si tomamos el inverso de la imagen mostrada arriba (en optimizaciones), podemos obtener un extraño efecto de AO.

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

Ad infinitum

Como puede ver, muchos efectos de procesamiento posterior se pueden implementar de manera trivial; juegue con diferentes funciones y vea qué otros efectos puede crear.

www.shadertoy.com/view/MtdBzs

¿Qué sigue?

Acabamos de cubrir los aspectos básicos aquí; hay mucho más que explorar en este campo, como:

  • Dispersión subsuperficial
  • Oclusión ambiental
  • Primitivas animadas
  • Funciones de deformación primitiva (twist, bend,…)
  • Transparencia (refracción, cáusticos,…)
  • Optimizaciones (jerarquías de volumen delimitadas)

Explore ShaderToy para inspirarse en lo que se puede hacer y explore varios sombreadores para ver cómo se implementan los diferentes efectos. Muchos sombreadores tienen variables que puede ajustar y ver instantáneamente los efectos de (alt-enter es el acceso directo para compilar!).

¡Lea también las referencias si está interesado en aprender más!

Gracias por leer! ¡Asegúrate de enviarnos tus geniales shaders! Si tienes algún comentario sobre el curso, ¡también nos encantaría escucharlo!

Contáctenos en twitter @liqwidice & @hdb1

¡Electric Square está contratando!

Lectura recomendada:

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

Demostración de Claybook: https://www.youtube.com/watch?v=Xpf7Ua3UqOA

Trazado de Rayos en un Fin de Semana: http://in1weekend.blogspot.com/2016/01/ray-tracing-in-one-weekend.html

Biblia de representación basada en la física, PBRT: https://www.pbrt.org/

Deja una respuesta

Tu dirección de correo electrónico no será publicada.