Shaders de juegos en 3D para principiantes

imagen

¿Quieres aprender a agregar texturas, iluminación, sombras, mapas normales, objetos brillantes, oclusión ambiental y otros efectos a tu juego 3D? Genial Este artículo presenta un conjunto de técnicas de sombreado que pueden elevar el nivel de los gráficos de su juego a nuevas alturas. Explico cada técnica de tal manera que puede aplicar / transferir esta información en cualquier pila de herramientas, ya sea Godot, Unity u otra cosa.

Como "pegamento" entre los sombreadores, decidí usar el magnífico motor de juegos Panda3D y OpenGL Shading Language (GLSL). Si usa la misma pila, obtendrá una ventaja adicional: aprenderá a usar técnicas de sombreado específicamente en Panda3D y OpenGL.

Preparación


A continuación se muestra el sistema que utilicé para desarrollar y probar el código de muestra.

Miercoles


El código de muestra se desarrolló y probó en el siguiente entorno:

  • Linux manjaro 4.9.135-1-MANJARO
  • Cadena de renderizador OpenGL: GeForce GTX 970 / PCIe / SSE2
  • Cadena de versión OpenGL: 4.6.0 NVIDIA 410.73
  • g ++ (GCC) 8.2.1 20180831
  • Panda3D 1.10.1-1

Materiales


Cada uno de los materiales de Blender utilizados para crear mill-scene.egg tiene dos texturas.

La primera textura es un mapa normal, la segunda es un mapa difuso. Si un objeto usa las normales de sus vértices, entonces se usa un mapa normal "azul claro". Debido al hecho de que todos los modelos tienen las mismas tarjetas en las mismas posiciones, los sombreadores se pueden generalizar y aplicar al nodo raíz del gráfico de escena.

Tenga en cuenta que el gráfico de escena es una característica de la implementación del motor Panda3D.


Aquí hay un mapa normal de un color que contiene solo el color [red = 128, green = 128, blue = 255] .

Este color indica la unidad normal, indicando en la dirección positiva del eje z [0, 0, 1] .

 [0, 0, 1] = [ round((0 * 0.5 + 0.5) * 255) , round((0 * 0.5 + 0.5) * 255) , round((1 * 0.5 + 0.5) * 255) ] = [128, 128, 255] = [ round(128 / 255 * 2 - 1) , round(128 / 255 * 2 - 1) , round(255 / 255 * 2 - 1) ] = [0, 0, 1] 

Aquí vemos la unidad normal [0, 0, 1] convertida a un color azul claro [128, 128, 255] , y el azul sólido convertido a una unidad normal.

Esto se describe con más detalle en la sección sobre técnicas de superposición de mapas normales.

Panda3d


En este ejemplo de código, Panda3D se usa como el "pegamento" entre los sombreadores. Esto no afecta las técnicas que se describen a continuación, es decir, puede usar la información estudiada aquí en cualquier pila o motor de juego seleccionado. Panda3D ofrece ciertas comodidades. En el artículo hablé sobre ellos, para que pueda encontrar su contraparte en su pila o recrearlos usted mismo si no están en la pila.

Vale la pena considerar que los valores gl-coordinate-system default , textures-power-2 down y textures-auto-power-2 1 se agregaron a config.prc . No están contenidos en la configuración estándar de Panda3D .

Por defecto, Panda3D usa un sistema de coordenadas diestro con un eje z hacia arriba, mientras que OpenGL usa un sistema de coordenadas diestro con un eje y hacia arriba.

gl-coordinate-system default le permite deshacerse de las transformaciones entre dos sistemas de coordenadas dentro de los sombreadores.

textures-auto-power-2 1 nos permite usar tamaños de textura que no son potencias de dos, si el sistema los admite.

Esto es conveniente al realizar SSAO o implementar otras técnicas dentro de una pantalla / ventana, ya que el tamaño de la pantalla / ventana generalmente no es una potencia de dos.

textures-power-2 down reduce el tamaño de las texturas a una potencia de dos si el sistema solo admite texturas con tamaños iguales a potencias de dos.

Código de ejemplo de compilación


Si desea ejecutar el código de muestra, primero debe compilarlo.

Panda3D se ejecuta en Linux, Mac y Windows.

Linux


Comience instalando el SDK Panda3D para su distribución.

Encuentra dónde están los encabezados y bibliotecas de Panda3D. Lo más probable es que se encuentren en /usr/include/panda3d/ y en /usr/lib/panda3d/ .

Luego clone este repositorio y navegue a su directorio.

git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners


Ahora compile el código fuente en un archivo de salida.

g++ \
-c main.cxx \
-o 3d-game-shaders-for-beginners.o \
-std=gnu++11 \
-O2 \
-I/usr/include/python2.7/ \
-I/usr/include/panda3d/


Después de crear el archivo de salida, cree un archivo ejecutable asociando el archivo de salida con sus dependencias.

g++ \
3d-game-shaders-for-beginners.o \
-o 3d-game-shaders-for-beginners \
-L/usr/lib/panda3d \
-lp3framework \
-lpanda \
-lpandafx \
-lpandaexpress \
-lp3dtoolconfig \
-lp3dtool \
-lp3pystub \
-lp3direct \
-lpthread


Consulte el manual de Panda3D para más información.

Mac


Comience instalando Panda3D SDK para Mac.

Encuentra dónde están los encabezados y las bibliotecas de Panda3D.

Luego clone el repositorio y navegue a su directorio.

git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners


Ahora compile el código fuente en un archivo de salida. Debe encontrar dónde están los directorios de inclusión en Python 2.7 y Panda3D.

clang++ \
-c main.cxx \
-o 3d-game-shaders-for-beginners.o \
-std=gnu++11 \
-g \
-O2 \
-I/usr/include/python2.7/ \
-I/Developer/Panda3D/include/


Después de crear el archivo de salida, cree un archivo ejecutable asociando el archivo de salida con sus dependencias.

Necesita encontrar dónde se encuentran las bibliotecas Panda3D.

clang++ \
3d-game-shaders-for-beginners.o \
-o 3d-game-shaders-for-beginners \
-L/Developer/Panda3D/lib \
-lp3framework \
-lpanda \
-lpandafx \
-lpandaexpress \
-lp3dtoolconfig \
-lp3dtool \
-lp3pystub \
-lp3direct \
-lpthread


Consulte el manual de Panda3D para más información.

Ventanas


Comience instalando Panda3D SDK para Windows.

Encuentra dónde están los encabezados y bibliotecas de Panda3D.

Clone este repositorio y navegue a su directorio.

git clone https://github.com/lettier/3d-game-shaders-for-beginners.git
cd 3d-game-shaders-for-beginners


Consulte el manual de Panda3D para más información.

Lanzar demo


Después de compilar el código de muestra, puede ejecutar el ejecutable o la demostración. Así es como se ejecutan en Linux o Mac.

./3d-game-shaders-for-beginners

Y entonces se ejecutan en Windows:

3d-game-shaders-for-beginners.exe

Control del teclado


La demostración tiene un control de teclado que le permite mover la cámara y cambiar el estado de varios efectos.

Movimiento


  • w - muévete profundamente en la escena.
  • a - gira la escena en el sentido de las agujas del reloj.
  • s : aléjate de la escena.
  • d - gira la escena en sentido antihorario.

Efectos conmutables


  • y - habilitar SSAO.
  • Shift + y - deshabilita SSAO.
  • u - inclusión de circuitos.
  • Shift + u : deshabilita los contornos.
  • i - habilita la floración.
  • Shift + i : deshabilita la floración.
  • o - habilitar mapas normales.
  • Shift + o : deshabilita los mapas normales.
  • p - inclusión de niebla.
  • Shift + p - apaga la niebla.
  • h - la inclusión de la profundidad de campo.
  • Shift + h - deshabilita la profundidad de campo.
  • j : habilitar la posterización.
  • Shift + j - deshabilita la posterización
  • k : habilita la pixelación.
  • Shift + k : deshabilita la pixelización.
  • l - afilado.
  • Shift + l : deshabilita la nitidez.
  • n inclusión de grano de película.
  • Shift + n - deshabilita el grano de la película.

Sistema de referencia


Antes de comenzar a escribir sombreadores, debe familiarizarse con los siguientes sistemas de referencia o sistemas de coordenadas. Todos ellos se reducen de donde se toman las coordenadas actuales del origen de la referencia (0, 0, 0) . Tan pronto como lo descubramos, podemos transformarlos usando algún tipo de matriz u otro espacio vectorial. Por lo general, si la salida de un sombreador no se ve bien, entonces la causa son los sistemas de coordenadas confusos.

Modelo



El sistema de coordenadas del modelo u objeto es relativo al origen del modelo. En los programas de modelado tridimensional, por ejemplo, en Blender, generalmente se coloca en el centro del modelo.

El mundo



El espacio mundial es relativo al origen de la escena / nivel / universo que creaste.

Revisar



El espacio de coordenadas de la vista es relativo a la posición activa de la cámara.

Recorte



Espacio de recorte relativo al centro del marco de la cámara. Todas las coordenadas en él son homogéneas y están en el intervalo (-1, 1) . X e y son paralelos a la película de la cámara, y la coordenada z es la profundidad.


Todos los vértices que no están dentro de los límites de la pirámide de visibilidad o el volumen de visibilidad de la cámara se cortan o descartan. Vemos cómo sucede esto con un cubo truncado detrás por el plano lejano de la cámara, y con un cubo ubicado a un lado.

Pantalla



El espacio de la pantalla es (generalmente) relativo a la esquina inferior izquierda de la pantalla. X cambia de cero al ancho de la pantalla. Y cambia de cero a la altura de la pantalla.

GLSL


En lugar de trabajar con una canalización de funciones fijas, utilizaremos una canalización de representación de GPU programable. Como es programable, nosotros mismos debemos pasarle el código del programa en forma de sombreadores. Un sombreador es un programa (generalmente pequeño) creado con una sintaxis similar al lenguaje C. Una canalización de representación de GPU programable consta de varios pasos que se pueden programar utilizando sombreadores. Los diferentes tipos de sombreadores incluyen sombreadores de vértices, sombreadores de teselación, sombreadores geométricos, de fragmentos y computacionales. Para usar las técnicas descritas en el artículo, es suficiente para nosotros usar vértices y fragmentos
etapas

 #version 140 void main() {} 

Aquí está el sombreador GLSL mínimo, que consta del número de versión GLSL y la función principal.

 #version 140 uniform mat4 p3d_ModelViewProjectionMatrix; in vec4 p3d_Vertex; void main() { gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; } 

Aquí está el sombreador de vértices truncados GLSL, que transforma el vértice de entrada en espacio de recorte y muestra esta nueva posición como una posición de vértice uniforme.

El procedimiento main no devuelve nada, porque es void , y la variable gl_Position es la salida en línea.

Dos palabras clave que vale la pena mencionar son: uniform y in .

La palabra clave uniform significa que esta variable global es la misma para todos los vértices. Panda3D establece p3d_ModelViewProjectionMatrix y para cada vértice es la misma matriz.

La palabra clave in significa que esta variable global se pasa al sombreador. Un sombreador de vértices obtiene cada vértice en el que se compone la geometría, al que se adjunta un sombreador de vértices.

 #version 140 out vec4 fragColor; void main() { fragColor = vec4(0, 1, 0, 1); } 

Aquí está el sombreador de fragmentos GLSL recortado, que muestra el verde opaco como el color del fragmento.

No olvide que un fragmento afecta solo a un píxel de la pantalla, pero varios fragmentos pueden afectar a un píxel.

Presta atención a la palabra clave out.

La palabra clave out significa que el sombreador establece esta variable global.

El nombre fragColor opcional, por lo que puede elegir cualquier otro.


Aquí está la salida de los dos sombreadores que se muestran arriba.

Representación de textura


En lugar de representar / dibujar directamente en la pantalla, el código de muestra utiliza una técnica para
el nombre "renderizar a textura" (renderizar a textura). Para renderizar a una textura, necesita configurar el búfer de marco y vincular la textura a él. Puede vincular múltiples texturas a un solo búfer de cuadro.

Las texturas ligadas al buffer de cuadro almacenan los vectores devueltos por el sombreador de fragmentos. Por lo general, estos vectores son vectores de color (r, g, b, a) , pero pueden ser vectores de posición o normales (x, y, z, w) . Para cada textura encuadernada, un sombreador de fragmentos puede generar un vector separado. Por ejemplo, podemos deducir de una pasada la posición y la normalidad del vértice.

La mayor parte del código de ejemplo que funciona con Panda3D está relacionado con la configuración de las texturas del búfer de cuadros . Para simplificar las cosas, cada sombreador de fragmentos en el código de ejemplo tiene solo una salida. Sin embargo, para garantizar una alta velocidad de fotogramas (FPS), necesitamos generar la mayor cantidad de información posible en cada paso de representación.

Aquí hay dos estructuras de textura para el buffer de cuadro del código de muestra.


La primera estructura convierte una escena de molino de agua en una textura de búfer de cuadro utilizando una variedad de sombreadores de vértices y fragmentos. Esta estructura atraviesa cada uno de los vértices de la etapa con el molino y a lo largo de los fragmentos correspondientes.

En esta estructura, el código de ejemplo funciona de la siguiente manera.

  • Guarda datos de geometría (por ejemplo, posición o vértice normal) para uso futuro.
  • Guarda datos de material (por ejemplo, color difuso) para uso futuro.
  • Crea un enlace UV de diferentes texturas (mapas difusos, normales, mapas de sombras, etc.).
  • Calcula la iluminación ambiental, difusa, reflejada y emitida.
  • Renderiza la niebla.


La segunda estructura es una cámara ortogonal dirigida a un rectángulo en forma de pantalla.
Esta estructura atraviesa solo cuatro picos y sus fragmentos correspondientes.

En la segunda estructura, el código de muestra realiza las siguientes acciones:

  • Procesa la salida de otra textura de búfer de cuadro.
  • Combina diferentes texturas de frame buffer en una.

En el ejemplo de código, podemos ver la salida de una textura de búfer de cuadro, configurando el cuadro correspondiente en verdadero y falso para todos los demás.

  // ... bool showPositionBuffer = false; bool showNormalBuffer = false; bool showSsaoBuffer = false; bool showSsaoBlurBuffer = false; bool showMaterialDiffuseBuffer = false; bool showOutlineBuffer = false; bool showBaseBuffer = false; bool showSharpenBuffer = false; bool showBloomBuffer = false; bool showCombineBuffer = false; bool showCombineBlurBuffer = false; bool showDepthOfFieldBuffer = false; bool showPosterizeBuffer = false; bool showPixelizeBuffer = false; bool showFilmGrainBuffer = true; // ... 

Texturizado



La textura es la unión de un color o algún otro vector a un fragmento utilizando coordenadas UV. Los valores de U y V varían de cero a uno. Cada vértice recibe una coordenada UV y se muestra en el sombreador de vértices.


El sombreador de fragmentos obtiene la coordenada UV interpolada. La interpolación significa que la coordenada UV para el fragmento está en algún lugar entre las coordenadas UV de los vértices que forman la cara del triángulo.

Sombreador de vértices


 #version 140 uniform mat4 p3d_ModelViewProjectionMatrix; in vec2 p3d_MultiTexCoord0; in vec4 p3d_Vertex; out vec2 texCoord; void main() { texCoord = p3d_MultiTexCoord0; gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex; } 

Aquí vemos que el sombreador de vértices genera la coordenada de la textura en el sombreador de fragmentos. Tenga en cuenta que este es un vector bidimensional: un valor para U y otro para V.

Sombreador de fragmentos


 #version 140 uniform sampler2D p3d_Texture0; in vec2 texCoord; out vec2 fragColor; void main() { texColor = texture(p3d_Texture0, texCoord); fragColor = texColor; } 

Aquí vemos que el sombreador de fragmentos busca el color en su coordenada UV y lo muestra como el color del fragmento.

Textura de relleno de pantalla


 #version 140 uniform sampler2D screenSizedTexture; out vec2 fragColor; void main() { vec2 texSize = textureSize(texture, 0).xy; vec2 texCoord = gl_FragCoord.xy / texSize; texColor = texture(screenSizedTexture, texCoord); fragColor = texColor; } 

Al renderizar a una textura, la malla es un rectángulo plano con la misma relación de aspecto que la pantalla. Por lo tanto, podemos calcular las coordenadas UV, sabiendo solo

A) el ancho y la altura de la textura con el tamaño de la pantalla superpuesta en el rectángulo usando coordenadas UV, y
B) las coordenadas x e y del fragmento.

Para unir x a U, debe dividir x por el ancho de la textura entrante. Del mismo modo, para unir y a V, debe dividir y por la altura de la textura entrante. Verá que esta técnica se utiliza en el código de muestra.

Iluminación



Para determinar la iluminación, es necesario calcular y combinar aspectos de la iluminación ambiental, difusa, reflejada y emitida. El código de muestra utiliza iluminación Phong.

Sombreador de vértices


 // ... uniform struct p3d_LightSourceParameters { vec4 color ; vec4 ambient ; vec4 diffuse ; vec4 specular ; vec4 position ; vec3 spotDirection ; float spotExponent ; float spotCutoff ; float spotCosCutoff ; float constantAttenuation ; float linearAttenuation ; float quadraticAttenuation ; vec3 attenuation ; sampler2DShadow shadowMap ; mat4 shadowViewMatrix ; } p3d_LightSource[NUMBER_OF_LIGHTS]; // ... 

Para cada fuente de luz, con la excepción de la luz ambiental, Panda3D nos proporciona una estructura conveniente que está disponible para sombreadores de vértices y fragmentos. Lo más conveniente es un mapa de sombras y una matriz para ver sombras para convertir los vértices en un espacio de sombras o iluminación.

  // ... vertexPosition = p3d_ModelViewMatrix * p3d_Vertex; // ... for (int i = 0; i < p3d_LightSource.length(); ++i) { vertexInShadowSpaces[i] = p3d_LightSource[i].shadowViewMatrix * vertexPosition; } // ... 

Comenzando con el sombreador de vértices, debemos transformar y eliminar el vértice del espacio de visualización en el espacio de sombra o iluminación para cada fuente de luz en la escena. Esto será útil en el futuro para que el sombreador de fragmentos genere sombras. Un espacio de sombra o iluminación es un espacio en el que cada coordenada es relativa a la posición de la fuente de luz (el origen es la fuente de luz).

Sombreador de fragmentos


El sombreador de fragmentos realiza la mayor parte del cálculo de la iluminación.

Material


 // ... uniform struct { vec4 ambient ; vec4 diffuse ; vec4 emission ; vec3 specular ; float shininess ; } p3d_Material; // ... 

Panda3D nos proporciona material (en forma de estructura) para la malla o modelo que estamos renderizando actualmente.

Múltiples fuentes de iluminación


  // ... vec4 diffuseSpecular = vec4(0.0, 0.0, 0.0, 0.0); // ... 

Antes de dar una vuelta por las fuentes de iluminación de la escena, crearemos una unidad que contendrá colores difusos y reflejados.

  // ... for (int i = 0; i < p3d_LightSource.length(); ++i) { // ... } // ... 

Ahora podemos recorrer las fuentes de luz en un ciclo, calculando los colores difusos y reflejados para cada uno.

Vectores relacionados con la iluminación



Aquí hay cuatro vectores básicos necesarios para calcular los colores difusos y reflejados introducidos por cada fuente de luz. El vector de dirección de iluminación es una flecha azul que apunta a la fuente de luz. El vector normal es una flecha verde que apunta verticalmente hacia arriba. El vector de reflexión es una flecha azul que refleja el vector de dirección de la luz. El vector de ojo o vista es la flecha naranja que apunta hacia la cámara.

  // ... vec3 lightDirection = p3d_LightSource[i].position.xyz - vertexPosition.xyz * p3d_LightSource[i].position.w; // ... 

La dirección de la iluminación es el vector desde la posición del vértice hasta la posición de la fuente de luz.

Si se trata de iluminación direccional, Panda3D establece p3d_LightSource[i].position.w cero. La iluminación direccional no tiene posición, solo dirección. Por lo tanto, si se trata de iluminación direccional, entonces la dirección de la iluminación será una dirección negativa u opuesta a la fuente, porque para la iluminación direccional Panda3D establece p3d_LightSource[i].position.xyz en p3d_LightSource[i].position.xyz .

  // ... normal = normalize(vertexNormal); // ... 

Lo normal al vértice debe ser un vector unitario. Los vectores unitarios tienen un valor igual a uno.

  // ... vec3 unitLightDirection = normalize(lightDirection); vec3 eyeDirection = normalize(-vertexPosition.xyz); vec3 reflectedDirection = normalize(-reflect(unitLightDirection, normal)); // ... 

A continuación, necesitamos tres vectores más.

Necesitamos un producto escalar con la participación de la dirección de iluminación, por lo que es mejor normalizarlo. Esto nos da una distancia o magnitud igual a la unidad (vector unitario).

La dirección de la vista es opuesta a la posición del vértice / fragmento, porque la posición del vértice / fragmento es relativa a la posición de la cámara. No olvide que la posición del vértice / fragmento está en el espacio de visualización. Por lo tanto, en lugar de pasar de la cámara (ojo) al vértice / fragmento, pasamos del vértice / fragmento a la cámara (ojo).

El vector de reflexión es un reflejo de la dirección de iluminación normal a la superficie. Cuando el "rayo" de luz toca la superficie, se refleja en el mismo ángulo en el que cayó. El ángulo entre el vector de dirección de la iluminación y el normal se llama "ángulo de incidencia". El ángulo entre el vector de reflexión y el normal se llama "ángulo de reflexión".

Debe cambiar el signo del vector de luz reflejada, ya que debe apuntar en la misma dirección que el vector del ojo. No olvide que la dirección del ojo va desde la parte superior / fragmento a la posición de la cámara. Usaremos el vector de reflexión para calcular el brillo de la luz reflejada.

Iluminación difusa


  // ... float diffuseIntensity = max(dot(normal, unitLightDirection), 0.0); if (diffuseIntensity > 0) { // ... } // ... 

El brillo de la iluminación difusa es el producto escalar de lo normal a la superficie y la dirección de iluminación de un solo vector. El producto escalar puede variar de menos uno a uno. Si ambos vectores apuntan en la misma dirección, entonces el brillo es la unidad. En todos los demás casos, será menos que la unidad.


Si el vector de iluminación se aproxima a la misma dirección que lo normal, entonces el brillo de la iluminación difusa tiende a la unidad.

Si el brillo de la iluminación difusa es menor o igual a cero, entonces debe pasar a la siguiente fuente de luz.

  // ... vec4 diffuse = vec4 ( clamp ( diffuseTex.rgb * p3d_LightSource[i].diffuse.rgb * diffuseIntensity , 0 , 1 ) , 1 ); diffuse.r = clamp(diffuse.r, 0, diffuseTex.r); diffuse.g = clamp(diffuse.g, 0, diffuseTex.g); diffuse.b = clamp(diffuse.b, 0, diffuseTex.b); // ... 

Ahora podemos calcular el color difuso introducido por esta fuente.Si el brillo de la iluminación difusa es igual a la unidad, entonces el color difuso será una mezcla del color de la textura difusa y el color de la iluminación. En cualquier otro brillo, el color difuso será más oscuro.

Tenga en cuenta que limito el color difuso para que no sea más brillante que el color de la textura difusa. Esto evitará la sobreexposición de la escena.

Luz reflejada


Después de una iluminación difusa, se calcula lo reflejado.


  // ... vec4 specular = clamp ( vec4(p3d_Material.specular, 1) * p3d_LightSource[i].specular * pow ( max(dot(reflectedDirection, eyeDirection), 0) , p3d_Material.shininess ) , 0 , 1 ); // ... 

El brillo de la luz reflejada es el producto escalar entre el vector del ojo y el vector de reflexión. Como en el caso del brillo de la iluminación difusa, si dos vectores apuntan en la misma dirección, entonces el brillo de la iluminación reflejada es igual a la unidad. Cualquier otro brillo reducirá la cantidad de color reflejado introducido por esta fuente de luz.


El brillo del material determina cuánto se dispersará la iluminación de la luz reflejada. Por lo general, se configura en un programa de simulación, por ejemplo, en Blender. En Blender, se llama dureza especular.

Focos


  // ... float unitLightDirectionDelta = dot ( normalize(p3d_LightSource[i].spotDirection) , -unitLightDirection ); if (unitLightDirectionDelta >= p3d_LightSource[i].spotCosCutoff) { // ... } // ... } 

Este código no permite que la iluminación afecte fragmentos fuera del cono de foco o la pirámide. Afortunadamente, Panda3D puede definir spotDirection y spotCosCutofftrabajar con las luces direccionales y spot. Los focos tienen una posición y una dirección. Sin embargo, la iluminación direccional solo tiene dirección, y las fuentes puntuales solo tienen posición. Sin embargo, este código funciona para los tres tipos de iluminación sin la necesidad de declaraciones if confusas.

 spotCosCutoff = cosine(0.5 * spotlightLensFovAngle); 

Si en el caso de la iluminación de proyección, el producto escalar del vector "fragmento-fuente de iluminación" y el vector de dirección del reflector es menor que el coseno de la mitad del ángulo del campo de visión del
reflector, entonces el sombreador no tiene en cuenta la influencia de esta fuente.

Tenga en cuenta que debe cambiar el signo unitLightDirection. unitLightDirectionva del fragmento al reflector, y necesitamos pasar del reflector al fragmento, porque spotDirectionva directamente al centro de la pirámide del reflector a cierta distancia de la posición del reflector.

En el caso de la iluminación direccional y puntual, Panda3D establece el spotCosCutoffvalor en -1. Recuerde que el producto escalar varía en el rango de -1 a 1. Por lo tanto, no importa cuál será unitLightDirectionDelta, porque siempre es mayor o igual que -1.

  // ... diffuse *= pow(unitLightDirectionDelta, p3d_LightSource[i].spotExponent); // ... 

Al igual que el código unitLightDirectionDelta, este código también funciona para los tres tipos de fuentes de luz. En el caso de los focos, hará que los fragmentos sean más brillantes a medida que se acerque al centro de la pirámide de focos. Para fuentes direccionales y puntuales de luz spotExponentes cero. Recuerde que cualquier valor de la potencia de cero es igual a la unidad, por lo que el color difuso es igual a sí mismo, multiplicado por uno, es decir, no cambia.

Sombras


  // ... float shadow = textureProj ( p3d_LightSource[i].shadowMap , vertexInShadowSpaces[i] ); diffuse.rgb *= shadow; specular.rgb *= shadow; // ... 

Panda3D simplifica el uso de las sombras porque crea un mapa de sombras y una matriz de transformación de sombras para cada fuente de luz en la escena. Para crear una matriz de transformación usted mismo, debe recopilar una matriz que convierta las coordenadas del espacio de visualización en el espacio de iluminación (las coordenadas son relativas a la posición de la fuente de luz). Para crear un mapa de sombras usted mismo, debe renderizar la escena desde el punto de vista de la fuente de luz en la textura del marco del búfer. La textura de la memoria intermedia del cuadro debe contener la distancia desde la fuente de luz hasta los fragmentos. Esto se llama un "mapa de profundidad". Finalmente, debe transferir manualmente al sombreador su mapa de profundidad casero como uniform sampler2DShadow, y la matriz de transformación de sombras como uniform mat4. Así que recrearemos lo que Panda3D hace automáticamente por nosotros.

Se utiliza el fragmento de código que se muestra textureProj, que es diferente de la función que se muestra arriba texture. textureProjprimero se divide vertexInShadowSpaces[i].xyzpor vertexInShadowSpaces[i].w. Luego lo usa vertexInShadowSpaces[i].xypara encontrar la profundidad almacenada en el mapa de sombras. Luego, ella vertexInShadowSpaces[i].zcompara la profundidad de la parte superior con la profundidad del mapa de sombras vertexInShadowSpaces[i].xy. Si la comparación tiene éxito, textureProjdevuelve uno. De lo contrario, devuelve cero. Cero significa que este vértice / fragmento está en la sombra, y uno significa que el vértice / fragmento no está en la sombra.

Tenga en cuenta que textureProjtambién puede devolver un valor de cero a uno, dependiendo de cómo esté configurado el mapa de sombras. En este ejemplotextureProjRealiza múltiples pruebas de profundidad basadas en profundidades adyacentes y devuelve un promedio ponderado. Este promedio ponderado puede dar suavidad a las sombras.

Atenuación



  // ... float lightDistance = length(lightDirection); float attenuation = 1 / ( p3d_LightSource[i].constantAttenuation + p3d_LightSource[i].linearAttenuation * lightDistance + p3d_LightSource[i].quadraticAttenuation * (lightDistance * lightDistance) ); diffuse.rgb *= attenuation; specular.rgb *= attenuation; // ... 

La distancia a la fuente de luz es simplemente la magnitud o la longitud del vector de dirección de la iluminación. Tenga en cuenta que no utilizamos la dirección normalizada de iluminación, porque esa distancia sería igual a la unidad.

La distancia a la fuente de luz es necesaria para calcular la atenuación. Atenuación significa que disminuye el efecto de la luz lejos de la fuente.

Parámetros constantAttenuation, linearAttenuationy quadraticAttenuationpuede establecer cualquier valor. Vale la pena comenzar con constantAttenuation = 1, linearAttenuation = 0y quadraticAttenuation = 1. Con estos parámetros, en la posición de la fuente de luz es igual a la unidad y tiende a cero cuando se aleja de ella.

Iluminación de color final


  // ... diffuseSpecular += (diffuse + specular); // ... 

Para calcular el color final de la iluminación, debe agregar el color difuso y reflejado. Es necesario agregar esto a la unidad en un ciclo de derivación de las fuentes de luz en la escena.

Ambiente


 // ... uniform sampler2D p3d_Texture1; // ... uniform struct { vec4 ambient ; } p3d_LightModel; // ... in vec2 diffuseCoord; // ... vec4 diffuseTex = texture(p3d_Texture1, diffuseCoord); // ... vec4 ambient = p3d_Material.ambient * p3d_LightModel.ambient * diffuseTex; // ... 

El componente de iluminación ambiental en el modelo de iluminación se basa en el color ambiental del material, el color de la iluminación ambiental y el color de la textura difusa.

Nunca debe haber más de una fuente de luz ambiental, por lo tanto, este cálculo debe realizarse solo una vez, en contraste con los cálculos de colores difusos y reflejados acumulados para cada fuente de luz.

Tenga en cuenta que el color de la luz ambiental es útil cuando se realiza SSAO.

Poniendo todo junto


  // ... vec4 outputColor = ambient + diffuseSpecular + p3d_Material.emission; // ... 

El color final es la suma del color ambiental, el color difuso, el color reflejado y el color emitido.

Código fuente



Mapas normales



El uso de mapas normales le permite agregar nuevas partes a la superficie sin geometría adicional. Normalmente, cuando se trabaja en un programa de modelado 3D, se crean versiones de malla alta y baja. Luego, se toman las normales de los vértices de la alta malla de polietileno y se hornean en la textura. Esta textura es un mapa normal. Luego, dentro del sombreador de fragmentos, reemplazamos las normales de los vértices de la malla de polietileno baja con las normales de la malla de polietileno alta cocidas en el mapa normal. Debido a esto, al encender una malla, parecerá que tiene más polígonos de los que realmente tiene. Esto le permite mantener un FPS alto, mientras transmite la mayoría de los detalles de la versión de alta poli.


Aquí vemos la transición de un modelo de alta poli a un modelo de baja poli, y luego a un modelo de baja poli con un mapa normal superpuesto.


Sin embargo, no olvide que superponer un mapa normal es solo una ilusión. En cierto ángulo, la superficie comienza a verse plana nuevamente.

Sombreador de vértices


 // ... uniform mat3 p3d_NormalMatrix; // ... in vec3 p3d_Normal; // ... in vec3 p3d_Binormal; in vec3 p3d_Tangent; // ... vertexNormal = normalize(p3d_NormalMatrix * p3d_Normal); binormal = normalize(p3d_NormalMatrix * p3d_Binormal); tangent = normalize(p3d_NormalMatrix * p3d_Tangent); // ... 

Comenzando con el sombreador de vértices, necesitamos generar el vector normal, el vector binormal y el vector tangente al sombreador de fragmentos. Estos vectores se utilizan en el sombreador de fragmentos para transformar la normalidad del mapa normal del espacio tangente al espacio de visualización.

p3d_NormalMatrixConvierte los vectores normales del vector de vértice, binormal y tangente en el espacio de visualización. No olvide que en el espacio de visualización todas las coordenadas son relativas a la posición de la cámara.

[p3d_NormalMatrix] son ​​los principales elementos de transposición inversa 3x3 de ModelViewMatrix. Esta estructura se utiliza para convertir el vector normal a las coordenadas del espacio de visualización.

Fuente

 // ... in vec2 p3d_MultiTexCoord0; // ... out vec2 normalCoord; // ... normalCoord = p3d_MultiTexCoord0; // ... 


También necesitamos enviar las coordenadas UV del mapa normal al sombreador de fragmentos.

Sombreador de fragmentos


Recuerde que el vértice normal se usó para calcular la iluminación. Sin embargo, para calcular la iluminación, el mapa normal nos da otras normales. En el sombreador de fragmentos, necesitamos reemplazar las normales de los vértices con las normales ubicadas en el mapa normal.

 // ... uniform sampler2D p3d_Texture0; // ... in vec2 normalCoord; // ... /* Find */ vec4 normalTex = texture(p3d_Texture0, normalCoord); // ... 

Usando las coordenadas del mapa normal transferido por el sombreador de vértices, extraemos la normal correspondiente del mapa.

  // ... vec3 normal; // ... /* Unpack */ normal = normalize ( normalTex.rgb * 2.0 - 1.0 ); // ... 

Arriba, mostré cómo las normales se convierten en colores para crear mapas normales. Ahora necesitamos revertir este proceso para que podamos obtener las normales originales horneadas en el mapa.

 [ r, g, b] = [ r * 2 - 1, g * 2 - 1, b * 2 - 1] = [ x, y, z] 

Así es como se ve el proceso de desempaquetar normales del mapa normal.

  // ... /* Transform */ normal = normalize ( mat3 ( tangent , binormal , vertexNormal ) * normal ); // ... 

Las normales obtenidas del mapa normal suelen estar en el espacio tangente. Sin embargo, pueden estar en otro espacio. Por ejemplo, Blender le permite hornear normales en espacio tangente, espacio de objetos, espacio mundial y espacio de cámara.


Para transferir la normalidad del mapa normal desde el espacio tangente al espacio de visualización, cree una matriz de 3x3 basada en el vector tangente, los vectores binormales y el vértice normal. Multiplique lo normal por esta matriz y normalícela. Aquí es donde terminamos con las normales. Todos los demás cálculos de iluminación aún se realizan.

Código fuente


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


All Articles