Implementación de nubes volumétricas físicamente correctas como en Horizon Zero Dawn

Anteriormente, las nubes en los juegos se dibujaban con sprites 2D normales, que siempre se giran en la dirección de la cámara, pero en los últimos años, los nuevos modelos de tarjetas de video le permiten dibujar nubes físicamente correctas sin pérdidas de rendimiento notables. Se cree que las nubes voluminosas en el juego trajeron al estudio Guerrilla Games junto con el juego Horizon Zero Dawn. Por supuesto, tales nubes pudieron renderizarse antes, pero el estudio formó algo así como un estándar de la industria para los recursos fuente y los algoritmos utilizados, y ahora cualquier implementación de nubes volumétricas de alguna manera cumple con este estándar.



Todo el proceso de renderizar nubes está muy bien dividido en etapas y es importante tener en cuenta que la implementación incorrecta, incluso en una de ellas, puede conducir a tales consecuencias que no estará claro dónde está el error y cómo solucionarlo, por lo tanto, es aconsejable hacer una conclusión de control del resultado cada vez.

Mapeo de tonos, sRGB


Antes de comenzar a trabajar con iluminación, es importante hacer dos cosas:

  1. Antes de mostrar la imagen final en la pantalla, aplique al menos el mapeo de tonos más simple:

    tunedColor=color/(1+color) 

    Esto es necesario porque los valores de color calculados serán mucho mayores que la unidad.
  2. Asegúrese de que el framebuffer final en el que está dibujando y que se muestra en la pantalla esté en formato sRGB. Si la activación del modo sRGB es un problema, la conversión se puede hacer manualmente en el sombreador:

     finalColor=pow(color, vec3(1.0/2.2)) 

    La fórmula es adecuada para la mayoría de los casos, pero no 100% según el monitor. Es importante que la conversión sRGB siempre se haga al final.

Modelo de iluminación


Considere un espacio lleno de materia parcialmente transparente de diferentes densidades. Cuando un rayo de luz atraviesa dicha sustancia, está expuesto a cuatro efectos: absorción, dispersión, dispersión amplificadora y autoradiación. Esto último ocurre en el caso de procesos químicos en una sustancia, y no se ve afectado aquí.

Supongamos que tenemos un rayo de luz que atraviesa la materia desde el punto A hasta el punto B:


Absorción

La luz que pasa a través de una sustancia es absorbida por esta misma sustancia. La fracción de luz no absorbida se puede encontrar mediante la fórmula:


donde - la luz que queda en el punto después de la absorción . - apunte en el segmento AB a distancia de A.

Dispersión

Parte de la luz bajo la influencia de partículas de materia cambia su dirección. La fracción de luz que no ha cambiado su dirección se puede encontrar mediante la fórmula:


donde - fracción de luz que no ha cambiado de dirección después de dispersarse en un punto .

Absorción y dispersión deben combinarse:


Función llamado atenuación o extinción. Una función - Función de transferencia. Muestra cuánta luz queda al pasar del punto A al punto B.

En cuanto a y : , donde C es una constante constante, que puede tener un valor diferente para cada canal en RGB, Es la densidad del medio en el punto .

Ahora vamos a complicar la tarea. La luz se mueve del punto A al punto B, se extingue durante el movimiento. En el punto X, parte de la luz se dispersa en diferentes direcciones, una de las direcciones corresponde al observador en el punto O. A continuación, una parte de la luz dispersa se mueve desde el punto X al punto O y se desintegra nuevamente. El camino de la luz AXO nos interesa.


La pérdida de luz al pasar de A a X sabemos: , tal como sabemos la pérdida de luz de X a O - esto . Pero, ¿qué pasa con la fracción de luz que se dispersará en la dirección del observador?

Dispersión de amplificación

Si en el caso de la dispersión ordinaria, la intensidad de la luz disminuye, entonces, en el caso de la dispersión amplificadora, aumenta debido a la dispersión de la luz que ha ocurrido en las regiones vecinas. La fórmula puede encontrar la cantidad total de luz proveniente de regiones vecinas:


donde significa tomar la integral sobre la esfera, - función de fase - luz procedente de la dirección .

Es bastante difícil calcular la luz desde todas las direcciones, sin embargo, sabemos que la porción original de luz es transportada por nuestro haz AB original. La fórmula se puede simplificar enormemente:


donde - el ángulo entre el haz de luz y el haz de observación (es decir, el ángulo AXO), - el valor inicial de la intensidad de la luz. Resumiendo todo lo anterior, obtenemos la fórmula:


donde - luz entrante - la luz llega al observador.

Complicamos la tarea un poco más. Digamos que la luz es emitida por una luz direccional, es decir. el sol


Todo sucede igual que en el caso anterior, pero muchas veces. La luz del punto A1 se dispersa en el punto X1 hacia el observador en el punto O, la luz del punto A2 se dispersa en el punto X2 hacia el observador en el punto O, etc. Vemos que la luz que llega al observador es igual a la suma:


O una expresión integral más precisa:


Es importante entender que aquí es decir El segmento se divide en un número infinito de secciones de longitud cero.

El cielo


Con una ligera simplificación, un rayo de sol que atraviesa la atmósfera se dispersa, es decir, .


Y ni siquiera un tipo de dispersión, sino dos: dispersión de Rayleigh y dispersión de Mi. La primera es causada por moléculas de aire, y la segunda es causada por un aerosol de agua.

La densidad total de aire (o aerosol) a través de la cual pasa un rayo de luz, que se mueve del punto A al punto B:
donde - altura de escala, h - altura actual.

Una solución integral simple sería:

donde dh es el tamaño del paso con el que se toma la muestra de altura.

Ahora mire la figura y use la fórmula derivada en la parte anterior del "modelo de iluminación":


El observador mira de O a O '. Queremos recolectar toda la luz que llega a los puntos X1, X2, ..., Xn, se dispersa en ellos y luego llega al observador:


donde la intensidad de la luz emitida por el sol - altura en el punto ; en el caso del cielo, constante C, que está en función denotado como .

La solución de la integral puede ser la siguiente:

Esta fórmula es válida tanto para la dispersión de Rayleigh como para la dispersión de Mie. Como resultado, los valores de luz para cada una de las dispersiones simplemente suman:


Dispersión de Rayleigh



(contiene valores para cada canal RGB)



Resultado:


Mi dispersión



(los valores para todos los canales RGB son iguales)



Resultado:


El número de muestras por segmento. y en el segmento Puedes tomar 32 y más. El radio de la Tierra es de 6371000 m, la atmósfera es de 100000 m.

Qué hacer con todo esto:

  1. En cada píxel de la pantalla, calculamos la dirección del observador V
  2. Tomamos la posición del observador O igual a {0, 6371000, 0}
  3. Nos encontramos como resultado de la intersección del rayo que se origina en el punto O, y la dirección de V y la esfera centrada en el punto {0,0,0} y un radio de 6471000
  4. Segmento de línea dividir en 32 secciones de igual longitud
  5. Para cada sección, calculamos la dispersión de Rayleigh y la dispersión de Mie, y agregamos todo. Además, para calcular también tendremos que dividir el segmento 32 parcelas iguales en cada caso. se puede leer a través de una variable, cuyo valor aumenta en cada paso del ciclo.

El resultado final:


Modelo de nube


Necesitaremos varios tipos de ruido en 3D. El primero es el ruido de movimiento browniano (fBm) al acecho fractal de Perlin:

Resultado para un corte 2D:


El segundo es el ruido de camuflaje fBm de Voronoi.

Resultado para un corte 2D:


Para obtener el ruido fBm de encubrimiento de Vorley, debe invertir el ruido fBm de encubrimiento de Voronoj. Sin embargo, cambié ligeramente los rangos de valores a mi criterio:

 float fbmTiledWorley3(...) { return clamp((1.0-fbmTiledVoronoi3(...))*1.5-0.25, 0.0, 1.0); } 

El resultado se parece inmediatamente a las estructuras de nubes:


Para las nubes, necesita obtener dos texturas especiales. El primero tiene un tamaño de 128x128x128 y es responsable del ruido de baja frecuencia, el segundo tiene un tamaño de 32x32x32 y es responsable del ruido de alta frecuencia. Cada textura usa solo un canal en formato R8. En algunos ejemplos, se usan 4 canales de R8G8B8A8 para la primera textura y tres canales de R8G8B8 para la segunda, y luego los canales se mezclan en un sombreador. No veo el punto, porque la mezcla se puede hacer de antemano, obteniendo así un mayor éxito en la coherencia de caché.

Para mezclar, y también en algunos lugares, se utilizará la función remap (), que escala los valores de un rango a otro:

 float remap(float value, float minValue, float maxValue, float newMinValue, float newMaxValue) { return newMinValue+(value-minValue)/(maxValue-minValue)*(newMaxValue-newMinValue); } 

Comencemos a preparar la textura con ruido de baja frecuencia:
Canal R - ruido fBm de perlin
Canal G - ruido de Vorley fBm en mosaico
Canal B: ruido de fBm Worley más pequeño con escala más pequeña
Canal A: ruido fBm taylable de Varley con una escala aún más pequeña


La mezcla se realiza de esta manera:

 finalValue=remap(noise.x, (noise.y * 0.625 + noise.z*0.25 + noise.w * 0.125)-1, 1, 0, 1) 

Resultado para un corte 2D:


Ahora prepare la textura con ruido de alta frecuencia:
Canal R - ruido de vorley fBm en mosaico
Canal G: ruido Vorley fBm a menor escala
Canal B - Varley taylivaya fBm ruido con escala aún más pequeña


 finalValue=noise.x * 0.625 + noise.y*0.25 + noise.z * 0.125; 

Resultado para un corte 2D:


También necesitamos un mapa de clima y texturas 2D que determinará la presencia, densidad y forma de las nubes, dependiendo de las coordenadas del espacio. Está pintado por artistas para afinar la cubierta de nubes. La interpretación de los canales de color del mapa meteorológico puede ser diferente, en la versión que presté, es la siguiente:


Canal R - nubosidad a baja altitud
Canal G - nubosidad a gran altitud
Canal B - altura máxima de la nube
Canal A - densidad de nubes

Ahora estamos listos para crear una función que devolverá la densidad de las nubes dependiendo de las coordenadas del espacio 3D.

En la entrada, un punto en el espacio con coordenadas en km.

 vec3 position 

Agregue inmediatamente el desplazamiento al viento

 position.xz+=vec2(0.2f)*ufmParams.time; 

Obtenga los valores del mapa meteorológico

 vec4 weather=textureLod(ufmWeatherMap, position.xz/4096.0f, 0); 
Obtenemos el porcentaje de altura (de 0 a 1)

 float height=cloudGetHeight(position); 

Agregue un pequeño redondeo de las nubes a continuación:
 float SRb=clamp(remap(height, 0, 0.07, 0, 1), 0, 1); 
Hacemos una disminución lineal de la densidad a 0 con el aumento de la altura de acuerdo con el canal B del mapa meteorológico:

 float SRt=clamp(remap(height, weather.b*0.2, weather.b, 1, 0), 0, 1); 
Combina el resultado:

 float SA=SRb*SRt; 

Nuevamente agregue el redondeo de las nubes a continuación:

 float DRb=height*clamp(remap(height, 0, 0.15, 0, 1), 0, 1); 

Agregue también el redondeo de las nubes en la parte superior:

 float DRt=height*clamp(remap(height, 0.9, 1, 1, 0), 0, 1); 
Combinamos el resultado, aquí agregamos la influencia de la densidad del mapa del tiempo y la influencia de la densidad, que se establece a través de la interfaz gráfica de usuario:

 float DA=DRb*DRt*weather.a*2*ufmProperties.density; 

Combina el ruido de baja frecuencia y alta frecuencia de nuestras texturas:

 float SNsample=textureLod(ufmLowFreqNoiseTexture, position/48.0f, 0).x*0.85f+textureLod(ufmHighFreqNoiseTexture, position/4.8f, 0).x*0.15f; 

En todos los documentos que leí, la fusión se lleva a cabo de una manera diferente, pero me gustó esta opción.

Determinamos la cantidad de cobertura (% del cielo ocupado por las nubes), que se establece a través de la interfaz gráfica de usuario, también se utilizan los canales R y G del mapa meteorológico:

 float WMc=max(weather.r, clamp(ufmProperties.coverage-0.5, 0, 1)*weather.g*2); 

Calcule la densidad final:

 float d=clamp(remap(SNsample*SA, 1-ufmProperties.coverage*WMc, 1, 0, 1), 0, 1)*DA; 

Función completa:

 float cloudSampleDensity(vec3 position) { position.xz+=vec2(0.2f)*ufmParams.time; vec4 weather=textureLod(ufmWeatherMap, position.xz/4096.0f+vec2(0.2, 0.1), 0); float height=cloudGetHeight(position); float SRb=clamp(remap(height, 0, 0.07, 0, 1), 0, 1); float SRt=clamp(remap(height, weather.b*0.2, weather.b, 1, 0), 0, 1); float SA=SRb*SRt; float DRb=height*clamp(remap(height, 0, 0.15, 0, 1), 0, 1); float DRt=height*clamp(remap(height, 0.9, 1, 1, 0), 0, 1); float DA=DRb*DRt*weather.a*2*ufmProperties.density; float SNsample=textureLod(ufmLowFreqNoiseTexture, position/48.0f, 0).x*0.85f+textureLod(ufmHighFreqNoiseTexture, position/4.8f, 0).x*0.15f; float WMc=max(weather.r, clamp(ufmProperties.coverage-0.5, 0, 1)*weather.g*2); float d=clamp(remap(SNsample*SA, 1-ufmProperties.coverage*WMc, 1, 0, 1), 0, 1)*DA; return d; } 

Cuál es exactamente esta función debería ser una pregunta abierta, porque ignorar las leyes que las nubes obedecen al establecer parámetros, puede obtener un resultado muy inusual y hermoso. Todo depende de la aplicación.


Integración


La atmósfera de la Tierra se divide en dos capas: interna y externa, entre las cuales se pueden ubicar las nubes. Estas capas se pueden representar por esferas, pero también por planos. Me instalé en las esferas. Para la primera capa, tomé el radio de la esfera de 6415 km, para la segunda capa, el radio de 6435 km. El radio de la tierra redondeó a 6400 km. Algunos parámetros dependerán del grosor condicional de la parte "nublada" de la atmósfera (20 km).



A diferencia del cielo, las nubes son opacas, y la integración requiere no solo obtener el color, sino también obtener el valor para el canal alfa. Primero necesita una función que devuelva la densidad total de la nube a través de la cual pasará un rayo de luz del sol.


Nadie llama la atención sobre esto, pero la práctica ha demostrado que no es necesario tener en cuenta toda la trayectoria del haz, solo se necesita la brecha más extrema. Suponemos que las nubes sobre un segmento truncado no existen en absoluto.


Además, estamos muy limitados en cuanto al número de muestras de densidad que se pueden hacer sin afectar el rendimiento. Guerrilla Games do 6. Además, en una de las presentaciones, el desarrollador dijo que esparcen estas muestras dentro del cono, y la última muestra se hace especialmente muy lejos del resto para cubrir la mayor cantidad de espacio posible. Las imprecisiones y el ruido resultantes se suavizarán en el contexto de las muestras vecinas, y esto, por el contrario, se convertirá en una mayor precisión.


Al final, me decidí por 4 muestras que se encuentran en la misma línea, pero esta última se toma con un aumento de 6 veces. El tamaño del escalón es de 20 km * 0.01, que es de 200 m.

La función es bastante simple:

 float cloudSampleDirectDensity(vec3 position, vec3 sunDir) { //   float avrStep=(6435.0-6415.0)*0.01; float sumDensity=0.0; for(int i=0;i<4;i++) { float step=avrStep; //      6 if(i==3) step=step*6.0; //  position+=sunDir*step; //  ,  ,   //  float density=cloudSampleDensity(position)*step; sumDensity+=density; } return sumDensity; } 

Ahora puedes pasar a la parte más difícil. Determinamos el observador en la superficie de la Tierra en el punto {0, 6400,0} y encontramos la intersección del haz de observación con una esfera de radio 6415 km y centro {0,0,0} - obtenemos el punto de partida S.


A continuación se muestra la versión básica de la función:

 vec4 mainMarching(vec3 viewDir, vec3 sunDir) { vec3 position; crossRaySphereOutFar(vec3(0.0, 6400.0, 0.0), viewDir, vec3(0.0), 6415.0, position); float avrStep=(6435.0-6415.0)/64.0; for(int i=0;i<128;i++) { position+=viewDir*step; if(length(position)>6435.0) break; } return vec4(0.0); } 

El tamaño del paso se define como 20 km / 64. en el caso de la dirección estrictamente vertical del haz del observador, haremos 64 muestras. Sin embargo, cuando esta dirección es más horizontal, las muestras serán un poco más grandes, por lo que no hay 64 pasos en el ciclo, sino 128 con un margen.

Al principio, asumimos que el color final es negro y la transparencia es la unidad. Con cada paso, aumentaremos el valor del color y disminuiremos el valor de la transparencia. Si la transparencia está cerca de 0, puede salir previamente del bucle:

 vec3 color=vec3(0.0); float transmittance=1.0; … //    //      float density=cloudSampleDensity(position)*avrStep; //   ,   //   float sunDensity=cloudSampleDirectDensity(position, sunDir); //      float m2=exp(-ufmProperties.attenuation*sunDensity); float m3=ufmProperties.attenuation2*density; float light=ufmProperties.sunIntensity*m2*m3; //       color+=sunColor*light*transmittance; transmittance*=exp(-ufmProperties.attenuation*density); … return vec4(color, 1.0-transmittance); 

ufmProperties.attenuation: no hay nada más que C en y ufmProperties.attenuation2 es C en . ufmProperties.sunIntensity: la intensidad de radiación del sol. sunColor: el color del sol.

Resultado:


Un defecto es inmediatamente evidente: sombreado severo. Pero ahora corregiremos la falta de iluminación amplificada cerca del sol. Sucedió porque no agregamos una función de fase. Para calcular la dispersión de la luz que pasa a través de las nubes, se utiliza la función de fase de Hengy-Greenstein, que la abrió en 1941 para cálculos similares en grupos de gases en el espacio:


Una digresión debe hacerse aquí. Según el modelo de iluminación canónica, la función de fase debe ser una. Sin embargo, en realidad, el resultado obtenido no se adapta a nadie y todos usan funciones de dos fases, e incluso combinan sus valores de una manera especial. También me concentré en funciones de dos fases, pero simplemente sumo sus valores. La función de la primera fase tiene g cerca de 1 y le permite hacer una iluminación brillante cerca del sol. La función de la segunda fase tiene g cerca de 0.5 y le permite hacer una disminución gradual de la iluminación en toda la esfera celeste.

Código actualizado:

 // cos(theta) float mu=max(0, dot(viewDir, sunDir)); float m11=ufmProperties.phaseInfluence*cloudPhaseFunction(mu, ufmProperties.eccentrisy); float m12=ufmProperties.phaseInfluence2*cloudPhaseFunction(mu, ufmProperties.eccentrisy2); float m2=exp(-ufmProperties.attenuation*sunDensity); float m3=ufmProperties.attenuation2*density; float light=ufmProperties.sunIntensity*(m11+m12)*m2*m3; 

ufmProperties.eccentrisy, ufmProperties.eccentrisy2 son valores g

Resultado:


Ahora puedes comenzar la pelea con demasiado sombreado. Está presente porque no tomamos en cuenta la luz de las nubes circundantes y el cielo, que es en la vida real.

Resolví este problema así:

 return vec4(color+ambientColor*ufmProperties.ambient, 1.0-transmittance); 

Donde ambientColor es el color del cielo en la dirección del haz de observación, ufmProperties.ambient es el parámetro de ajuste.

Resultado:


Queda por resolver el último problema. En la vida real, cuanto más horizontal es la vista, más vemos cierta niebla o neblina que no nos permite ver objetos muy distantes. Esto también debe reflejarse en el código. Tomé el coseno habitual del ángulo de la mirada y la función exponencial. En base a esto, se calcula un cierto coeficiente de mezcla, que permite la interpolación lineal entre el color resultante y el color de fondo.

 float blending=1.0-exp(-max(0.0, dot(viewDir, vec3(0.0,1.0,0.0)))*ufmProperties.fog); blending=blending*blending*blending; return vec4(mix(ambientColor, color+ambientColor*ufmProperties.ambient, blending), 1.0-transmittance); 

ufmProperties.fog: para la configuración manual.


Función de resumen:

 vec4 mainMarching(vec3 viewDir, vec3 sunDir, vec3 sunColor, vec3 ambientColor) { vec3 position; crossRaySphereOutFar(vec3(0.0, 6400.0, 0.0), viewDir, vec3(0.0), 6415.0, position); float avrStep=(6435.0-6415.0)/64.0; vec3 color=vec3(0.0); float transmittance=1.0; for(int i=0;i<128;i++) { float density=cloudSampleDensity(position)*avrStep; if(density>0.0) { float sunDensity=cloudSampleDirectDensity(position, sunDir); float mu=max(0.0, dot(viewDir, sunDir)); float m11=ufmProperties.phaseInfluence*cloudPhaseFunction(mu, ufmProperties.eccentrisy); float m12=ufmProperties.phaseInfluence2*cloudPhaseFunction(mu, ufmProperties.eccentrisy2); float m2=exp(-ufmProperties.attenuation*sunDensity); float m3=ufmProperties.attenuation2*density; float light=ufmProperties.sunIntensity*(m11+m12)*m2*m3; color+=sunColor*light*transmittance; transmittance*=exp(-ufmProperties.attenuation*density); } position+=viewDir*avrStep; if(transmittance<0.05 || length(position)>6435.0) break; } float blending=1.0-exp(-max(0.0, dot(viewDir, vec3(0.0,1.0,0.0)))*ufmProperties.fog); blending=blending*blending*blending; return vec4(mix(ambientColor, color+ambientColor*ufmProperties.ambient, blending), 1.0-transmittance); } 

Video de demostración:


Optimización y posibles mejoras.


Después de implementar el algoritmo de renderizado básico, el siguiente problema es que funciona muy lentamente. Mi versión produjo 25 fps en full hd en la radeon rx 480. Los dos siguientes enfoques para resolver el problema fueron sugeridos por los mismos Guerrilla Games.

Dibujamos lo que es realmente visible.

La pantalla está dividida en mosaicos de 16x16 píxeles de tamaño. Primero, se dibuja el entorno 3D habitual. Resulta que la mayor parte del cielo está cubierto por montañas u objetos grandes. En consecuencia, debe realizar el cálculo solo en aquellos mosaicos en los que las nubes no están bloqueadas por nada.

Reproyección

Cuando la cámara está parada, resulta que las nubes en general no se pueden actualizar. Sin embargo, si la cámara se ha movido, esto no significa que debamos actualizar toda la pantalla. Todo ya está dibujado, solo necesita reconstruir la imagen de acuerdo con las nuevas coordenadas. Encontrar las coordenadas antiguas en las nuevas, a través de la proyección y ver las matrices de los cuadros actuales y anteriores, se llama proyección. Por lo tanto, en el caso de un cambio de cámara, simplemente transferimos los colores de acuerdo con las nuevas coordenadas. En los casos en que estas coordenadas indican fuera de la pantalla, las nubes deben volver a dibujarse honestamente.

Actualización parcial

No me gusta la idea de la reproyección porque con un giro brusco de la cámara puede resultar que las nubes tendrán que representarse para un tercio de la pantalla, lo que puede causar retraso. No sé cómo Guerrilla Games lidió con esto, pero al menos en Horizon Zero Dawn, cuando se controla el joystick, la cámara se mueve suavemente y no hay problemas con saltos bruscos. Por lo tanto, como experimento, se me ocurrió mi propio enfoque. Las nubes se dibujan en un mapa cúbico, en 5 caras, porque el fondo no nos interesa. El lado del mapa cúbico tiene una resolución reducida igual a ⅔ de la altura de la pantalla.Cada cara del mapa cúbico se divide en 8x8 fichas. Cada cuadro en cada cara se actualiza con solo uno de 64 píxeles en cada mosaico. Esto produce artefactos notables durante los cambios repentinos, pero porque las nubes son bastante estáticas, entonces ese truco es invisible. Como resultado, la radeon rx 480 produce 500 fps en full hd para el volcán y 330 fps para opengl. La serie Radeon hd 5700 produce 109 fps en full hd bajo opengl (vulkan no es compatible).

Uso de niveles de mip

Al acceder a texturas con ruido, puede tomar datos del nivel de mip cero solo en las primeras muestras, y luego, cuanto más lejos estén las muestras, mayor será el nivel de mip.

Nubes altas

Para simular la presencia de altitud de cirrus y nubes de cirrocúmulos en Guerrilla Games durante la integración, las últimas muestras no están hechas de las texturas 3D de las que hablé, sino de una textura 2D especial.


Ruido

de rizo Se utilizan varias texturas adicionales en el ruido de rizo para crear el efecto del viento que sopla las nubes. Estas texturas son necesarias para cambiar las coordenadas originales.


Rayos divinos


Dichos rayos, que atrapan dramas, se realizan en el posprocesamiento. Primero, se dibuja una iluminación brillante alrededor del sol, donde no está bloqueada por las nubes. Entonces esta luz de fondo debe estar radialmente desviada del sol.


Ahora necesita aplicar suavizado radial.


De hecho, hay muchas más mejoras y sutilezas, pero no las verifiqué todas, por lo que no puedo decir con confianza sobre ellas. Sin embargo, puedes familiarizarte con ellos tú mismo. La más fuerte, creo, es la documentación en la nube del motor Frostbite.

Enlaces utiles


Guerrilla Games
d1z4o56rleaq4j.cloudfront.net/downloads/assets/Nubis-Authoring-Realtime-Volumetric-Cloudscapes-with-the-Decima-Engine-Final.pdf?mtime=20170807141817
killzone.dl.playstation.net/killzone/horizonzerodawn/presentations/Siggraph15_Schneider_Real-Time_Volumetric_Cloudscapes_of_Horizon_Zero_Dawn.pdf
www.youtube.com/watch?v=-d8qT5-1LOI

GPU Pro 7
vk.com/doc179245989_437393482?hash=a9af5f665eda4edf58&dl=806d4dbdac0f7a761c


www.scratchapixel.com/lessons/procedural-generation-virtual-worlds/simulating-sky/simulating-colors-of-the-sky

Frostbite
media.contentapi.ea.com/content/dam/eacom/frostbite/files/s2016-pbs-frostbite-sky-clouds-new.pdf
www.shadertoy.com/view/XlBSRz

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


All Articles