Advertisement
  1. Game Development
  2. WebGL

Creación de sombreadores con Babylon.js y WebGL: teoría y ejemplos

Scroll to top
Read Time: 22 min
Sponsored Content

This sponsored post features a product relevant to our readers while meeting our editorial guidelines for being objective and educational.

Spanish (Español) translation by Elías Nicolás (you can also view the original English article)

En la presentación para el Día 2 de //Build 2014 (vea 2: 24-2: 28), los evangelistas de Microsoft Steven Guggenheimer y John Shewchuk demostraron cómo el soporte de Oculus Rift se agregó a Babylon.js. Y una de las cosas clave para esta demostración fue el trabajo que hicimos en un sombreador específico para simular lentes, como puede ver en esta imagen:

Lens simulation imageLens simulation imageLens simulation image

También presenté una sesión con Frank Olivier y Ben Constable sobre gráficos en IE y Babylon.js.

Esto me lleva a una de las preguntas que la gente a menudo me hace sobre Babylon.js: "¿Qué quieres decir con shaders?" Por lo tanto, en esta publicación, voy a explicarte cómo funcionan los sombreadores y a dar algunos ejemplos de los tipos comunes de sombreadores.

La teoría

Antes de comenzar a experimentar, primero debemos ver cómo funcionan las cosas internamente.

Cuando se trata de 3D acelerado por hardware, estamos discutiendo dos CPU: la CPU principal y la GPU. La GPU es una especie de CPU extremadamente especializada.

La GPU es una máquina de estado que configura usando la CPU. Por ejemplo, la CPU configurará la GPU para representar líneas en lugar de triángulos. O definirá que la transparencia está activada, y así sucesivamente.

Una vez que se hayan establecido todos los estados, la CPU definirá qué renderizar: la geometría, que se compone de una lista de puntos (llamados vértices y almacenados en una matriz llamada buffer de vértices) y una lista de índices (las caras, o triángulos, almacenados en una matriz llamada índice de buffer).

El paso final para la CPU es definir cómo renderizar la geometría, y para esta tarea específica, la CPU definirá sombreadores para la GPU. Los sombreadores son una pieza de código que la GPU ejecutará para cada uno de los vértices y píxeles que debe representar.

Primero, algo de vocabulario: piense en un vértice (vértices cuando hay varios) como un "punto" en un entorno 3D (en oposición a un punto en un entorno 2D).

Hay dos tipos de sombreadores: sombreadores de vértices y sombreadores de píxeles (o fragmentos).

Gráficos Pipeline

Antes de profundizar en los sombreadores, demos un paso atrás. Para representar píxeles, la GPU tomará la geometría definida por la CPU y hará lo siguiente:

Usando el buffer de índice, se juntan tres vértices para definir un triángulo: el buffer de índice contiene una lista de índices de vértices. Esto significa que cada entrada en el búfer de índice es el número de un vértice en el búfer de vértice. Esto es realmente útil para evitar la duplicación de vértices.

Por ejemplo, el siguiente buffer de índice es una lista de dos caras: [1 2 3 1 3 4]. La primera cara contiene el vértice 1, el vértice 2 y el vértice 3. La segunda cara contiene el vértice 1, el vértice 3 y el vértice 4. Entonces, hay cuatro vértices en esta geometría:

Chart showing four verticesChart showing four verticesChart showing four vertices

El sombreador de vértices se aplica en cada vértice del triángulo. El objetivo principal del sombreado de vértices es producir un píxel para cada vértice (la proyección en la pantalla 2D del vértice 3D):

vertex shader is applied on each vertex of the trianglevertex shader is applied on each vertex of the trianglevertex shader is applied on each vertex of the triangle

Usando estos tres píxeles (que definen un triángulo 2D en la pantalla), la GPU interpolará todos los valores asociados al píxel (al menos su posición), y el sombreador de píxeles se aplicará a cada píxel incluido en el triángulo 2D para generar un color para cada píxel:

pixel shader will be applied on every pixel included into the 2D trianglepixel shader will be applied on every pixel included into the 2D trianglepixel shader will be applied on every pixel included into the 2D triangle

Este proceso se realiza para cada cara definida por el buffer de índice.

Obviamente, debido a su naturaleza paralela, la GPU es capaz de procesar este paso para muchas caras simultáneamente, y así lograr un rendimiento realmente bueno.

GLSL

Acabamos de ver que para renderizar triángulos, la GPU necesita dos sombreadores: el sombreador de vértices y el sombreador de píxeles. Estos sombreadores están escritos usando un lenguaje llamado GLSL (Graphics Shader Language). Parece que C.

Para Internet Explorer 11, hemos desarrollado un compilador para transformar GLSL a HLSL (High Level Shader Language), que es el lenguaje de sombreado de DirectX 11. Esto permite que IE11 se asegure de que el código de sombreado sea seguro (no desea usarlo). WebGL para reiniciar su computadora!):

Flow chart of transforming GLSL to HLSLFlow chart of transforming GLSL to HLSLFlow chart of transforming GLSL to HLSL

Aquí hay una muestra de un sombreador de vértices común:

1
precision highp float;
2
 
3
 // Attributes
4
 attribute vec3 position;
5
 attribute vec2 uv;
6
 
7
 // Uniforms
8
 uniform mat4 worldViewProjection;
9
 
10
 // Varying
11
 varying vec2 vUV;
12
 
13
 void main(void) {
14
     gl_Position = worldViewProjection * vec4(position, 1.0);
15
 
16
     vUV = uv;
17
 }

Estructura de sombreado vértice

Un sombreador de vértices contiene lo siguiente:

  • Atributos: un atributo define una parte de un vértice. Por defecto, un vértice debería contener al menos una posición (un vector3:x, y, z). Pero como desarrollador, puede decidir agregar más información. Por ejemplo, en el shader anterior, hay un vector2  llamado uv (coordenadas de textura que nos permiten aplicar una textura 2D en un objeto 3D).
  • Uniformes: Un uniforme es una variable utilizada por el sombreador y definida por la CPU. El único uniforme que tenemos aquí es una matriz utilizada para proyectar la posición del vértice (x, y, z) a la pantalla (x, y).
  • Variable: las variables "variables" son valores creados por el sombreador de vértices y transmitidos al sombreador de píxeles. Aquí, el sombreador de vértices transmitirá un valor de vUV (una copia simple de uv) al sombreador de píxeles. Esto significa que un píxel se define aquí con una posición y coordenadas de textura. Estos valores serán interpolados por la GPU y utilizados por el sombreador de píxeles.
  • main: la función llamada main() es el código ejecutado por la GPU para cada vértice y debe al menos producir un valor para gl_position (la posición en la pantalla del vértice actual).

Podemos ver en nuestra muestra que el sombreador de vértices es bastante simple. Genera una variable del sistema (comenzando con gl_) llamada gl_position para definir la posición del píxel asociado, y establece una variable variable llamada vUV.

El vudú detrás de las matrices

En nuestro shader tenemos una matriz llamada worldViewProjection. Usamos esta matriz para proyectar la posición del vértice a la variable gl_position. Eso es genial, pero ¿cómo obtenemos el valor de esta matriz? Es un uniforme, por lo que debemos definirlo en el lado de la CPU (usando JavaScript).

Esta es una de las partes complejas de hacer 3D. Debe comprender matemática compleja (o tendrá que usar un motor 3D, como Babylon.js, que veremos más adelante).

La matriz worldViewProjection es la combinación de tres matrices diferentes:

The worldViewProjection matrix is the combination of three different matricesThe worldViewProjection matrix is the combination of three different matricesThe worldViewProjection matrix is the combination of three different matrices

El uso de la matriz resultante nos permite transformar vértices 3D en píxeles 2D teniendo en cuenta el punto de vista y todo lo relacionado con la posición / escala / rotación del objeto actual.

Esta es su responsabilidad como desarrollador 3D: crear y mantener esta matriz actualizada.

De vuelta a los sombreadores

Una vez que el sombreador de vértices se ejecuta en cada vértice (tres veces, entonces) tenemos tres píxeles con una gl_position correcta y un valor vUV. La GPU interpolará estos valores en cada píxel contenido en el triángulo producido por estos píxeles.

Luego, para cada píxel, ejecutará el sombreador de píxeles:

1
precision highp float;
2
 varying vec2 vUV;
3
 uniform sampler2D textureSampler;
4
 
5
 void main(void) {
6
     gl_FragColor = texture2D(textureSampler, vUV);
7
 }

Pixel (o Fragmento) Estructura de Shader

La estructura de un sombreador de píxeles es similar a un sombreador de vértices:

  • Variable: las variables "variables" son valores creados por el sombreador de vértices y transmitidos al sombreador de píxeles. Aquí el sombreador de píxeles recibió un valor vUV del sombreador de vértices.
  • Uniformes: uniformes es una variable utilizada por el sombreador y definido por la CPU. El único uniforme que tenemos aquí es una muestra, usada para leer colores de textura.
  • main: la función denominada main es el código ejecutado por la GPU para cada píxel y debe al menos producir un valor para gl_FragColor (el color del píxel real).

Este sombreador de píxeles es bastante simple: lee el color de la textura que usa las coordenadas de la textura del sombreador de vértices.

¿Quieres ver el resultado de tal sombreador? Aquí está:

Esto se está procesando en tiempo real; Puedes arrastrar la esfera con tu mouse.

Para lograr este resultado, tendrá que lidiar con una gran cantidad de código WebGL. De hecho, WebGL es una API muy poderosa pero de muy bajo nivel, y usted tiene que hacer todo por su cuenta, desde crear los tres hasta definir las estructuras de vértices. También tiene que hacer todos los cálculos, configurar todos los estados y manejar la carga de texto, etc.

¿Demasiado? BABYLON.ShaderMaterial al rescate

Sé lo que estás pensando: los sombreadores son geniales, pero no quiero molestarme con las tuberías internas de WebGL o incluso con las matemáticas.

¡Y eso está bien! Esta es una pregunta perfecta legítima, y ​​esa es exactamente la razón por la que cree Babylon.js.

Permítanme presentarles el código utilizado por la demostración anterior de rolling sphere. Antes que nada, necesita una página web simple:

1
<!DOCTYPE html>
2
 <html>
3
 <head>
4
     <title>Babylon.js</title>
5
     <script src="Babylon.js"></script>
6
     
7
     <script type="application/vertexShader" id="vertexShaderCode">
8
         precision highp float;
9
 
10
         // Attributes

11
         attribute vec3 position;
12
         attribute vec2 uv;
13
 
14
         // Uniforms

15
         uniform mat4 worldViewProjection;
16
 
17
         // Normal

18
         varying vec2 vUV;
19
 
20
         void main(void) {
21
         gl_Position = worldViewProjection * vec4(position, 1.0);
22
 
23
         vUV = uv;
24
         }
25
     </script>
26
   
27
     <script type="application/fragmentShader" id="fragmentShaderCode">
28
         precision highp float;
29
         varying vec2 vUV;
30
 
31
         uniform sampler2D textureSampler;
32
 
33
         void main(void) {
34
         gl_FragColor = texture2D(textureSampler, vUV);
35
         }
36
     </script>
37
 
38
     <script src="index.js"></script>
39
     <style>
40
         html, body {
41
             width: 100%;
42
             height: 100%;
43
             padding: 0;
44
             margin: 0;
45
             overflow: hidden;
46
             margin: 0px;
47
             overflow: hidden;
48
         }
49
 
50
         #renderCanvas {
51
             width: 100%;
52
             height: 100%;
53
             touch-action: none;
54
             -ms-touch-action: none;
55
         }
56
     </style>
57
 </head>
58
 <body>
59
     <canvas id="renderCanvas"></canvas>
60
 </body>
61
 </html>

Notará que los sombreadores están definidos por etiquetas <script>. Con Babylon.js también puede definirlos en archivos separados (archivos .fx).

Puede obtener Babylon.js aquí o en nuestro repositorio de GitHub. Debe usar la versión 1.11 o superior para obtener acceso a BABYLON.StandardMaterial.

Y, finalmente, el código JavaScript principal es el siguiente:

1
"use strict";
2
 
3
 document.addEventListener("DOMContentLoaded", startGame, false);
4
 
5
 function startGame() {
6
     if (BABYLON.Engine.isSupported()) {
7
         var canvas = document.getElementById("renderCanvas");
8
         var engine = new BABYLON.Engine(canvas, false);
9
         var scene = new BABYLON.Scene(engine);
10
         var camera = new BABYLON.ArcRotateCamera("Camera", 0, Math.PI / 2, 10, BABYLON.Vector3.Zero(), scene);
11
 
12
         camera.attachControl(canvas);
13
 
14
         // Creating sphere

15
         var sphere = BABYLON.Mesh.CreateSphere("Sphere", 16, 5, scene);
16
 
17
         var amigaMaterial = new BABYLON.ShaderMaterial("amiga", scene, {
18
             vertexElement: "vertexShaderCode",
19
             fragmentElement: "fragmentShaderCode",
20
         },
21
         {
22
             attributes: ["position", "uv"],
23
             uniforms: ["worldViewProjection"]
24
         });
25
         amigaMaterial.setTexture("textureSampler", new BABYLON.Texture("amiga.jpg", scene));
26
 
27
         sphere.material = amigaMaterial;
28
 
29
         engine.runRenderLoop(function () {
30
             sphere.rotation.y += 0.05;
31
             scene.render();
32
         });
33
     }
34
 };

Puede ver que uso un BABYLON.ShaderMaterial para deshacerse de toda la carga de compilar, vincular y manejar sombreadores.

Cuando crea un BABYLON.ShaderMaterial, debe especificar el elemento DOM utilizado para almacenar los sombreadores o el nombre base de los archivos donde están los shaders. Si elige usar archivos, debe crear un archivo para cada sombreador y usar el siguiente patrón de nombre de archivo: basename.vertex.fx y basename.fragment.fx. Entonces tendrás que crear el material así:

1
var cloudMaterial = new BABYLON.ShaderMaterial("cloud", scene, "./myShader",
2
         {
3
             attributes: ["position", "uv"],
4
             uniforms: ["worldViewProjection"]
5
         });

También debe especificar los nombres de los atributos y uniformes que usa. Luego, puede establecer directamente el valor de sus uniformes y muestras utilizando las funciones setTexture, setFloat, setFloats, setColor3, setColor4, setVector2, setVector3, setVector4, y setMatrix.

Bastante simple, ¿verdad?

¿Recuerdas la matriz worldViewProjection anterior? Usando Babylon.js y BABYLON.ShaderMaterial, ¡no tienes de qué preocuparte! BABYLON.ShaderMaterial lo calculará automáticamente porque lo declaras en la lista de uniformes.

BABYLON.ShaderMaterial también puede manejar las siguientes matrices para ti:

  • world
  • view
  • projection
  • worldView
  • worldViewProjection

No hay necesidad de matemáticas por más tiempo. Por ejemplo, cada vez que ejecutas sphere.rotation.y += 0.05, la matriz mundial de la esfera se genera para ti y se transmite a la GPU.

CYOS: Crea tu propio sombreador

Así que vayamos más grandes y creemos una página donde pueda crear dinámicamente sus propios sombreadores y ver el resultado de inmediato. Esta página usará el mismo código que hemos discutido anteriormente, y usará un objeto BABYLON.ShaderMaterial para compilar y ejecutar los sombreadores que usted creará.

Utilicé el editor de código ACE para CYOS. Este es un editor de código increíble con marcadores de sintaxis. Siéntete libre de echarle un vistazo aquí. Puede encontrar CYOS aquí.

Usando el primer cuadro combinado, podrá seleccionar sombreadores predefinidos. Veremos cada uno de ellos inmediatamente después.

También puede cambiar la malla (el objeto 3D) utilizada para obtener una vista previa de los sombreadores utilizando el segundo cuadro combinado.

El botón Compile se usa para crear un nuevo BABYLON.ShaderMaterial de sus sombreadores. El código utilizado por este botón es el siguiente:

1
// Compile
2
 shaderMaterial = new BABYLON.ShaderMaterial("shader", scene, {
3
     vertexElement: "vertexShaderCode",
4
     fragmentElement: "fragmentShaderCode",
5
 },
6
     {
7
         attributes: ["position", "normal", "uv"],
8
         uniforms: ["world", "worldView", "worldViewProjection"]
9
     });
10
 
11
 var refTexture = new BABYLON.Texture("ref.jpg", scene);
12
 refTexture.wrapU = BABYLON.Texture.CLAMP_ADDRESSMODE;
13
 refTexture.wrapV = BABYLON.Texture.CLAMP_ADDRESSMODE;
14
 
15
 var amigaTexture = new BABYLON.Texture("amiga.jpg", scene);
16
 
17
 shaderMaterial.setTexture("textureSampler", amigaTexture);
18
 shaderMaterial.setTexture("refSampler", refTexture);
19
 shaderMaterial.setFloat("time", 0);
20
 shaderMaterial.setVector3("cameraPosition", BABYLON.Vector3.Zero());
21
 shaderMaterial.backFaceCulling = false;
22
 
23
 mesh.material = shaderMaterial;

Brutalmente simple, ¿verdad? El material está listo para enviarle tres matrices pre calculadas (world, worldView y worldViewProjection). Los vértices vendrán con coordenadas de posición, normal y de textura. Dos texturas también están cargadas para ti:

amiga textureamiga textureamiga texture
amiga.jpg
ref textureref textureref texture
ref.jpg

Y finalmente, aquí está el renderLoop donde actualizo dos convenientes uniformes:

  • uno llamado time para obtener animaciones divertidas
  • uno llamado cameraPosition para obtener la posición de la cámara en sus shaders (que será útil para las ecuaciones de iluminación)
1
engine.runRenderLoop(function () {
2
     mesh.rotation.y += 0.001;
3
 
4
     if (shaderMaterial) {
5
         shaderMaterial.setFloat("time", time);
6
         time += 0.02;
7
 
8
         shaderMaterial.setVector3("cameraPosition", camera.position);
9
     }
10
 
11
     scene.render();
12
 });

Gracias al trabajo que hicimos en Windows Phone 8.1, también puedes usar CYOS en tu Windows Phone (siempre es un buen momento para crear un shader):

CYOS on Windows PhoneCYOS on Windows PhoneCYOS on Windows Phone

Shader básico

Comencemos con el primer sombreador definido en CYOS: el sombreador básico.

Ya conocemos este sombreador. Calcula la gl_position y usa coordenadas de textura para obtener un color para cada píxel.

Para calcular la posición de píxel, solo necesitamos la matriz worldViewProjection y la posición del vértice:

1
precision highp float;
2
 
3
 // Attributes
4
 attribute vec3 position;
5
 attribute vec2 uv;
6
 
7
 // Uniforms
8
 uniform mat4 worldViewProjection;
9
 
10
 // Varying
11
 varying vec2 vUV;
12
 
13
 void main(void) {
14
     gl_Position = worldViewProjection * vec4(position, 1.0);
15
 
16
     vUV = uv;
17
 }

Las coordenadas de textura (uv) se transmiten sin modificaciones al sombreador de píxeles.

Tenga en cuenta que debemos agregar precision mediump float; en la primera línea para sombreadores de vértices y píxeles porque Chrome lo requiere. Define que, para un mejor rendimiento, no usamos valores flotantes de precisión completa.

El sombreador de píxeles es aún más simple, porque solo necesitamos usar coordenadas de textura y obtener un color de textura:

1
precision highp float;
2
 
3
 varying vec2 vUV;
4
 
5
 uniform sampler2D textureSampler;
6
 
7
 void main(void) {
8
     gl_FragColor = texture2D(textureSampler, vUV);
9
 }

Vimos anteriormente que el uniforme textureSampler está relleno con la textura "amiga", por lo que el resultado es el siguiente:

Basic Shader resultBasic Shader resultBasic Shader result

Shader blanco y negro

Ahora continuemos con un nuevo sombreador: el sombreador blanco y negro.

El objetivo de este sombreado es utilizar el anterior pero con un modo de representación "solo en blanco y negro". Para hacerlo, podemos mantener el mismo sombreado de vértices, pero el sombreador de píxeles debe modificarse ligeramente.

La primera opción que tenemos es tomar solo un componente, como el verde:

1
precision highp float;
2
 
3
 varying vec2 vUV;
4
 
5
 uniform sampler2D textureSampler;
6
 
7
 void main(void) {
8
     gl_FragColor = vec4(texture2D(textureSampler, vUV).ggg, 1.0);
9
 }

Como puede ver, en lugar de usar .rgb (esta operación se conoce como swizzle), usamos .ggg.

Pero si queremos un efecto blanco y negro realmente preciso, sería una mejor idea calcular la luminancia (que tiene en cuenta todos los componentes de color):

1
precision highp float;
2
 
3
 varying vec2 vUV;
4
 
5
 uniform sampler2D textureSampler;
6
 
7
 void main(void) {
8
     float luminance = dot(texture2D(textureSampler, vUV).rgb, vec3(0.3, 0.59, 0.11));
9
     gl_FragColor = vec4(luminance, luminance, luminance, 1.0);
10
 }

La operación de punto (o producto de punto) se calcula de esta manera:

result = v0.x * v1.x + v0.y * v1.y + v0.z * v1.z

Entonces en nuestro caso:

luminance = r * 0.3 + g * 0.59 + b * 0.11 (estos valores se basan en el hecho de que el ojo humano es más sensible al verde)

Suena genial, ¿verdad?

Black and white shader resultBlack and white shader resultBlack and white shader result

Shader de sombreado de céldas

Ahora pasemos a un sombreador más complejo: el sombreador de sombreado de celda.

Este requerirá que obtengamos la posición normal del vértice y la posición del vértice en el sombreador de píxeles. Entonces el sombreador de vértices se verá así:

1
precision highp float;
2
 
3
 // Attributes
4
 attribute vec3 position;
5
 attribute vec3 normal;
6
 attribute vec2 uv;
7
 
8
 // Uniforms
9
 uniform mat4 world;
10
 uniform mat4 worldViewProjection;
11
 
12
 // Varying
13
 varying vec3 vPositionW;
14
 varying vec3 vNormalW;
15
 varying vec2 vUV;
16
 
17
 void main(void) {
18
     vec4 outPosition = worldViewProjection * vec4(position, 1.0);
19
     gl_Position = outPosition;
20
 
21
     vPositionW = vec3(world * vec4(position, 1.0));
22
     vNormalW = normalize(vec3(world * vec4(normal, 0.0)));
23
 
24
     vUV = uv;
25
 }

Tenga en cuenta que también usamos la matriz mundial, porque la posición y la normalidad se almacenan sin ninguna transformación y debemos aplicar la matriz mundial para tener en cuenta la rotación del objeto.

El sombreador de píxeles es el siguiente:

1
precision highp float;
2
 
3
 // Lights
4
 varying vec3 vPositionW;
5
 varying vec3 vNormalW;
6
 varying vec2 vUV;
7
 
8
 // Refs
9
 uniform sampler2D textureSampler;
10
 
11
 void main(void) {
12
     float ToonThresholds[4];
13
     ToonThresholds[0] = 0.95;
14
     ToonThresholds[1] = 0.5;
15
     ToonThresholds[2] = 0.2;
16
     ToonThresholds[3] = 0.03;
17
 
18
     float ToonBrightnessLevels[5];
19
     ToonBrightnessLevels[0] = 1.0;
20
     ToonBrightnessLevels[1] = 0.8;
21
     ToonBrightnessLevels[2] = 0.6;
22
     ToonBrightnessLevels[3] = 0.35;
23
     ToonBrightnessLevels[4] = 0.2;
24
 
25
     vec3 vLightPosition = vec3(0, 20, 10);
26
 
27
     // Light
28
     vec3 lightVectorW = normalize(vLightPosition - vPositionW);
29
 
30
     // diffuse
31
     float ndl = max(0., dot(vNormalW, lightVectorW));
32
 
33
     vec3 color = texture2D(textureSampler, vUV).rgb;
34
 
35
     if (ndl > ToonThresholds[0])
36
     {
37
         color *= ToonBrightnessLevels[0];
38
     }
39
     else if (ndl > ToonThresholds[1])
40
     {
41
         color *= ToonBrightnessLevels[1];
42
     }
43
     else if (ndl > ToonThresholds[2])
44
     {
45
         color *= ToonBrightnessLevels[2];
46
     }
47
     else if (ndl > ToonThresholds[3])
48
     {
49
         color *= ToonBrightnessLevels[3];
50
     }
51
     else
52
     {
53
         color *= ToonBrightnessLevels[4];
54
     }
55
 
56
     gl_FragColor = vec4(color, 1.);
57
 }

El objetivo de este sombreador es simular una luz y, en lugar de calcular un sombreado uniforme, consideraremos que la luz se aplicará de acuerdo con los umbrales de brillo específicos. Por ejemplo, si la intensidad de la luz está entre 1 (máximo) y 0,95, el color del objeto (extraído de la textura) se aplicará directamente. Si la intensidad está entre 0.95 y 0.5, el color será atenuado por un factor de 0.8, y así sucesivamente.

Entonces, hay principalmente cuatro pasos en este sombreador:

  •     Primero, declaramos umbrales y niveles constantes.    
  • Entonces, necesitamos calcular la iluminación usando la ecuación de Phong (suponemos que la luz no se mueve):
1
vec3 vLightPosition = vec3(0, 20, 10);
2
 
3
 // Light
4
 vec3 lightVectorW = normalize(vLightPosition - vPositionW);
5
 
6
 // diffuse
7
 float ndl = max(0., dot(vNormalW, lightVectorW));

La intensidad de la luz por píxel depende del ángulo entre la dirección normal y la de la luz.

  • Luego obtenemos el color de textura para el píxel.
  • Y finalmente, verificamos el umbral y aplicamos el nivel al color.

El resultado parece un objeto de caricatura:

Cell shading shader resultCell shading shader resultCell shading shader result

Phong Shader

Usamos una porción de la ecuación de Phong en el sombreador anterior. Intentemos usar todo ahora.

El sombreador de vértices es claramente simple aquí, porque todo se hará en el sombreador de píxeles:

1
precision highp float;
2
 
3
 // Attributes
4
 attribute vec3 position;
5
 attribute vec3 normal;
6
 attribute vec2 uv;
7
 
8
 // Uniforms
9
 uniform mat4 worldViewProjection;
10
 
11
 // Varying
12
 varying vec3 vPosition;
13
 varying vec3 vNormal;
14
 varying vec2 vUV;
15
 
16
 void main(void) {
17
     vec4 outPosition = worldViewProjection * vec4(position, 1.0);
18
     gl_Position = outPosition;
19
 
20
     vUV = uv;
21
     vPosition = position;
22
     vNormal = normal;
23
 }

De acuerdo con la ecuación, debes calcular la parte difusa y especular usando la dirección de la luz y la normal del vértice:

1
precision highp float;
2
 
3
 // Varying
4
 varying vec3 vPosition;
5
 varying vec3 vNormal;
6
 varying vec2 vUV;
7
 
8
 // Uniforms
9
 uniform mat4 world;
10
 
11
 // Refs
12
 uniform vec3 cameraPosition;
13
 uniform sampler2D textureSampler;
14
 
15
 void main(void) {
16
     vec3 vLightPosition = vec3(0, 20, 10);
17
 
18
     // World values
19
     vec3 vPositionW = vec3(world * vec4(vPosition, 1.0));
20
     vec3 vNormalW = normalize(vec3(world * vec4(vNormal, 0.0)));
21
     vec3 viewDirectionW = normalize(cameraPosition - vPositionW);
22
 
23
     // Light
24
     vec3 lightVectorW = normalize(vLightPosition - vPositionW);
25
     vec3 color = texture2D(textureSampler, vUV).rgb;
26
 
27
     // diffuse
28
     float ndl = max(0., dot(vNormalW, lightVectorW));
29
 
30
     // Specular
31
     vec3 angleW = normalize(viewDirectionW + lightVectorW);
32
     float specComp = max(0., dot(vNormalW, angleW));
33
     specComp = pow(specComp, max(1., 64.)) * 2.;
34
 
35
     gl_FragColor = vec4(color * ndl + vec3(specComp), 1.);
36
 }

Ya usamos la parte difusa en el sombreador anterior, así que aquí solo tenemos que agregar la parte especular. Esta imagen de un artículo de Wikipedia explica cómo funciona el sombreador:

Diffuse plus Specular equals Phong ReflectionDiffuse plus Specular equals Phong ReflectionDiffuse plus Specular equals Phong Reflection
Por Brad Smith, también conocido como Rainwarrior.

El resultado en nuestra esfera:

Phong shader resultPhong shader resultPhong shader result

Descartar Shader

Para el descarte de sombreador, me gustaría introducir un nuevo concepto: la palabra clave discard. Este sombreador descartará todos los píxeles no rojos y creará la ilusión de un objeto "excavado".

El sombreador de vértices es el mismo que el utilizado por el sombreador básico:

1
precision highp float;
2
 
3
 // Attributes
4
 attribute vec3 position;
5
 attribute vec3 normal;
6
 attribute vec2 uv;
7
 
8
 // Uniforms
9
 uniform mat4 worldViewProjection;
10
 
11
 // Varying
12
 varying vec2 vUV;
13
 
14
 void main(void) {
15
     gl_Position = worldViewProjection * vec4(position, 1.0);
16
 
17
     vUV = uv;
18
 }

El sombreador de píxeles tendrá que probar el color y usar discard cuando, por ejemplo, el componente verde sea demasiado alto:

1
precision highp float;
2
 
3
 varying vec2 vUV;
4
 
5
 // Refs
6
 uniform sampler2D textureSampler;
7
 
8
 void main(void) {
9
     vec3 color = texture2D(textureSampler, vUV).rgb;
10
 
11
     if (color.g > 0.5) {
12
         discard;
13
     }
14
 
15
     gl_FragColor = vec4(color, 1.);
16
 }

El resultado es gracioso:

Discard shader resultDiscard shader resultDiscard shader result

Shader Onda

Hemos jugado mucho con los sombreadores de píxeles, pero también quería mostrarles que podemos hacer muchas cosas con los sombreadores de vértices.

Para el sombreador de onda, reutilizaremos el sombreador de píxeles Phong.

El sombreador de vértices usará el uniforme llamado time para obtener algunos valores animados. Usando este uniforme, el sombreador generará una onda con las posiciones de los vértices:

1
precision highp float;
2
 
3
 // Attributes
4
 attribute vec3 position;
5
 attribute vec3 normal;
6
 attribute vec2 uv;
7
 
8
 // Uniforms
9
 uniform mat4 worldViewProjection;
10
 uniform float time;
11
 
12
 // Varying
13
 varying vec3 vPosition;
14
 varying vec3 vNormal;
15
 varying vec2 vUV;
16
 
17
 void main(void) {
18
     vec3 v = position;
19
     v.x += sin(2.0 * position.y + (time)) * 0.5;
20
 
21
     gl_Position = worldViewProjection * vec4(v, 1.0);
22
 
23
     vPosition = position;
24
     vNormal = normal;
25
     vUV = uv;
26
 }

Se aplica un seno a position.y, y el resultado es el siguiente:

Wave shader resultWave shader resultWave shader result

Mapeo de entorno esférico

Este fue en gran parte inspirado por este tutorial. Te dejaré leer ese excelente artículo y jugar con el sombreador asociado.

Spherical environment mapping shaderSpherical environment mapping shaderSpherical environment mapping shader

Fresnel Shader

Me gustaría terminar este artículo con mi favorito: el sombreador Fresnel.

Este sombreador se usa para aplicar una intensidad diferente según el ángulo entre la dirección de visión y la normal del vértice.

El sombreador de vértices es el mismo utilizado por el sombreador de sombreado de células, y podemos calcular fácilmente el término Fresnel en nuestro sombreador de píxeles (porque tenemos la posición normal y la de la cámara, que se puede usar para evaluar la dirección de la vista):

1
precision highp float;
2
 
3
 // Lights
4
 varying vec3 vPositionW;
5
 varying vec3 vNormalW;
6
 
7
 // Refs
8
 uniform vec3 cameraPosition;
9
 uniform sampler2D textureSampler;
10
 
11
 void main(void) {
12
     vec3 color = vec3(1., 1., 1.);
13
     vec3 viewDirectionW = normalize(cameraPosition - vPositionW);
14
 
15
     // Fresnel
16
     float fresnelTerm = dot(viewDirectionW, vNormalW);
17
     fresnelTerm = clamp(1.0 - fresnelTerm, 0., 1.);
18
 
19
     gl_FragColor = vec4(color * fresnelTerm, 1.);
20
 }
Fresnel Shader resultFresnel Shader resultFresnel Shader result

¿Tu Shader?

Ahora estás más preparado para crear tu propio sombreador. Siéntase libre de usar los comentarios aquí o en el foro Babylon.js ¡para compartir sus experimentos!

Si quieres ir más lejos, aquí hay algunos enlaces útiles:

Y algo más de aprendizaje que he creado sobre el tema:

O, dando un paso atrás, la serie de aprendizaje de nuestro equipo sobre JavaScript:

Y, por supuesto, siempre puede utilizar algunas de nuestras herramientas gratuitas para crear su próxima experiencia web: Visual Studio Community, Azure Trial y herramientas de prueba entre navegadores para Mac, Linux o Windows.

Este artículo es parte de la tecnología de desarrollo webserie de Microsoft. Nos complace compartir con usted Microsoft Edge y el nuevo motor de renderizado EdgeHTML. Obtenga máquinas virtuales o prueba de forma remota en su Mac, iOS, Android o dispositivo de Windows @ http://dev.modern.ie/.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Game Development tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.