En mi tutorial
"Creación de sombreadores", observé principalmente los sombreadores de fragmentos, que son suficientes para implementar cualquier efecto 2D y ejemplos en
ShaderToy . Pero hay toda una categoría de técnicas que requieren el uso de sombreadores de vértices. En este tutorial, hablaré sobre la creación de un sombreador de agua estilizado de dibujos animados y le presentaré los sombreadores de vértices. También hablaré sobre el búfer de profundidad y cómo usarlo para obtener más información sobre la escena y crear líneas de espuma marina.
Así se verá el efecto final. Una demostración interactiva se puede ver
aquí .
Este efecto consta de los siguientes elementos:
- Una malla de agua translúcida con polígonos subdivididos y vértices desplazados para crear olas.
- Líneas de agua estáticas en la superficie.
- Flotabilidad simulada del barco.
- Líneas dinámicas de espuma alrededor de los límites de los objetos en el agua.
- Postprocesamiento para crear distorsión de todo bajo el agua.
En este sentido, me gusta el hecho de que toca muchos conceptos diferentes de gráficos por computadora, por lo que nos permitirá utilizar las ideas de los tutoriales anteriores, así como desarrollar técnicas que pueden aplicarse en nuevos efectos.
En este tutorial,
usaré PlayCanvas , simplemente porque es un conveniente IDE web gratuito, pero todo se puede aplicar a cualquier otro entorno WebGL sin ningún problema. Al final del artículo, se presentará la versión del código fuente para Three.js. Asumiremos que ya conoce bien los sombreadores de fragmentos y la interfaz de PlayCanvas. Puede actualizar su conocimiento sobre sombreadores
aquí y familiarizarse con PlayCanvas
aquí .
Entorno
El propósito de esta sección es configurar nuestro proyecto PlayCanvas e insertar en él varios objetos ambientales que influirán en el agua.
Si no tiene una cuenta de PlayCanvas,
regístrela y cree un nuevo
proyecto en blanco . Por defecto, debe tener un par de objetos en la escena, una cámara y una fuente de luz.
Insertar modelos
Un gran recurso para encontrar modelos 3D para la web es el proyecto Google
Poly . Tomé el
modelo de barco desde allí. Después de descargar y descomprimir el archivo, encontrará archivos
.obj
y
.png
en él.
- Arrastre ambos archivos a la ventana Activos del proyecto PlayCanvas.
- Seleccione el material generado automáticamente y seleccione el archivo
.png
como su mapa difuso.
Ahora puede arrastrar
Tugboat.json a la escena y eliminar los objetos Box y Plane. Si el barco parece demasiado pequeño, puede aumentar su escala (configuro el valor en 50).
Del mismo modo, puede agregar cualquier otro modelo a la escena.
Cámara en órbita
Para configurar la cámara volando en órbita,
copiaremos el script de
este ejemplo de PlayCanvas . Siga el enlace y haga clic en
Editor para abrir el proyecto.
- Copie el contenido de
mouse-input.js
y orbit-camera.js
de este proyecto tutorial en archivos con los mismos nombres de su proyecto. - Agregue un componente de secuencia de comandos a la cámara.
- Adjunte dos guiones a la cámara.
Sugerencia: para organizar el proyecto, puede crear carpetas en la ventana Activos. Puse estos dos guiones de cámara en la carpeta Guiones / Cámara /, mi modelo en Modelos / y el material en la carpeta Materiales /.
Ahora, cuando comienzas el juego (el botón de inicio en la parte superior derecha de la ventana de la escena) deberías ver un bote que puedes inspeccionar con una cámara moviéndolo en órbita con el mouse.
División de polígonos de superficie de agua
El propósito de esta sección es crear una malla subdividida que se utilizará como la superficie del agua.
Para crear una superficie de agua, adaptamos parte del código del
tutorial de generación de relieve . Cree un nuevo
Water.js
script
Water.js
. Abra este script para editarlo y cree una nueva función
GeneratePlaneMesh
que se verá así:
Water.prototype.GeneratePlaneMesh = function(options){
Ahora podemos llamarlo en la función de
initialize
:
Water.prototype.initialize = function() { this.GeneratePlaneMesh({subdivisions:100, width:10, height:10}); };
Ahora, cuando comienzas el juego, solo deberías ver una superficie plana. Pero esto no es solo una superficie plana, es una malla formada por miles de picos. Como ejercicio, intente verificar esto usted mismo (esta es una buena razón para estudiar el código que acaba de copiar).
Problema 1: cambie la coordenada Y de cada vértice por un valor aleatorio para que el plano se vea como la figura a continuación.
Las olas
El propósito de esta sección es designar la superficie del agua de su propio material y crear ondas animadas.
Para obtener los efectos que necesitamos, debe configurar su propio material. La mayoría de los motores 3D tienen un conjunto de sombreadores predefinidos para renderizar objetos y una forma de redefinirlos. Aquí hay un
buen enlace sobre cómo hacer esto en PlayCanvas.
Accesorio de sombreador
CreateWaterMaterial
una nueva función
CreateWaterMaterial
que
CreateWaterMaterial
nuevo material con un sombreador modificado y lo devuelva:
Water.prototype.CreateWaterMaterial = function(){
Esta función toma el código de sombreador de vértices y fragmentos de los atributos del script. Así que definamos en la parte superior del archivo (después de la línea
pc.createScript
):
Water.attributes.add('vs', { type: 'asset', assetType: 'shader', title: 'Vertex Shader' }); Water.attributes.add('fs', { type: 'asset', assetType: 'shader', title: 'Fragment Shader' });
Ahora podemos crear estos archivos de sombreador y adjuntarlos a nuestro script. Regrese al editor y cree dos archivos de sombreador:
Water.frag y
Water.vert . Adjunte estos sombreadores al script como se muestra en la figura a continuación.
Si los nuevos atributos no se muestran en el editor, haga clic en el botón
Analizar para actualizar el script.
Ahora pegue este sombreador básico en
Water.frag :
void main(void) { vec4 color = vec4(0.0,0.0,1.0,0.5); gl_FragColor = color; }
Y este está en
Water.vert :
attribute vec3 aPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; void main(void) { gl_Position = matrix_viewProjection * matrix_model * vec4(aPosition, 1.0); }
Finalmente, regrese a
Water.js para usar nuestro nuevo material en lugar del material estándar. Es decir, en lugar de:
var material = new pc.StandardMaterial();
insertar:
var material = this.CreateWaterMaterial();
Ahora, después de comenzar el juego, el avión debería ser azul.
Reinicio en caliente
Por ahora, acabamos de configurar espacios en blanco para nuestro nuevo material. Antes de comenzar a escribir efectos reales, quiero configurar la recarga automática de código.
Habiendo comentado la función de
swap
en cualquier archivo de script (por ejemplo, en Water.js), habilitaremos la recarga en caliente. Más adelante veremos cómo usar esto para mantener el estado incluso al actualizar el código en tiempo real. Pero por ahora, solo queremos volver a aplicar los sombreadores después de realizar los cambios. Antes de ejecutar en WebGL, se compilan los sombreadores, por lo que para hacer esto necesitamos volver a crear nuestro material.
Verificaremos si el contenido de nuestro código de sombreador ha cambiado y, de ser así, crearemos el material nuevamente. Primero, guarde los sombreadores actuales en
initialize :
Y en la
actualización verificamos si se han producido cambios:
Ahora, para asegurarte de que esto funciona, inicia el juego y cambia el color del avión en
Water.frag a un azul más agradable. Después de guardar el archivo, debe actualizarse incluso sin reiniciar y reiniciar. Aquí está el color que elegí:
vec4 color = vec4(0.0,0.7,1.0,0.5);
Sombreadores de vértices
Para crear ondas, debemos mover cada vértice de nuestra malla en cada cuadro. Parece que será muy ineficiente, pero cada vértice de cada modelo ya está transformado en cada cuadro renderizado. Esto es lo que hace el sombreador de vértices.
Si percibimos un sombreador de fragmentos como una función que se ejecuta para cada píxel, obtiene su posición y devuelve el color, entonces un
sombreador de vértice es una función que se ejecuta para cada píxel, obtiene su posición y devuelve
su posición .
Un sombreador de vértices por defecto obtiene una
posición en el mundo modelo y devuelve su
posición en la pantalla . Nuestra escena 3D se establece en coordenadas x, y y z, pero el monitor es un plano plano bidimensional, por lo que proyectamos un mundo 3D en una pantalla 2D. Las matrices del tipo, proyección y modelo están involucradas en dicha proyección, por lo tanto, no lo consideraremos en este tutorial. Pero si desea comprender qué sucede exactamente en cada etapa, aquí hay una
muy buena guía .
Es decir, esta línea:
gl_Position = matrix_viewProjection * matrix_model * vec4(aPosition, 1.0);
recibe
aPosition
como una posición en el mundo 3D de un vértice particular y lo convierte en
gl_Position
, es decir, en la posición final en la pantalla 2D. El prefijo "a" en aPosition indica que este valor es un
atributo . No olvide que la variable
uniforme es un valor que podemos definir en la CPU y pasarlo al sombreador. Mantiene el mismo valor para todos los píxeles / vértices. Por otro lado, el valor del atributo se obtiene de la
matriz de CPU especificada. Se llama un sombreador de vértices para cada valor de esta matriz de atributos.
Puede ver que estos atributos están configurados en la definición de sombreador que configuramos en Water.js:
var shaderDefinition = { attributes: { aPosition: pc.gfx.SEMANTIC_POSITION, aUv0: pc.SEMANTIC_TEXCOORD0, }, vshader: vertexShader, fshader: fragmentShader };
PlayCanvas se encarga de configurar y transmitir una matriz de posiciones de vértice para una
aPosition
al pasar esta enumeración, pero en el caso general, podemos pasar cualquier matriz de datos al sombreador de vértices.
Movimiento de vértices
Supongamos que queremos comprimir todo el plano multiplicando todos los valores de
x
por 0.5. ¿Necesitamos cambiar
aPosition
o
gl_Position
?
Probemos una
aPosition
primero. No podemos cambiar el atributo directamente, pero podemos crear una copia:
attribute vec3 aPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; void main(void) { vec3 pos = aPosition; pos.x *= 0.5; gl_Position = matrix_viewProjection * matrix_model * vec4(pos, 1.0); }
Ahora el avión debería verse más como un rectángulo. Y no hay nada extraño al respecto. Pero, ¿qué sucede si intentamos cambiar
gl_Position
?
attribute vec3 aPosition; uniform mat4 matrix_model; uniform mat4 matrix_viewProjection; void main(void) { vec3 pos = aPosition;
Hasta que empiece a mover la cámara, puede verse igual. Cambiamos las coordenadas del espacio de la pantalla, es decir, la imagen dependerá de
cómo la veamos .
Entonces podemos mover los vértices y, al mismo tiempo, es importante distinguir entre el trabajo en el mundo y los espacios de la pantalla.
Tarea 2: ¿puedes mover toda la superficie del plano varias unidades hacia arriba (a lo largo del eje Y) en el sombreador de vértices sin distorsionar su forma?
Tarea 3: Dije que gl_Position es bidimensional, pero gl_Position.z también existe. ¿Puede verificar si este valor afecta algo y, de ser así, para qué se utiliza?
Agregar tiempo
Lo último que necesitamos antes de comenzar a crear ondas en movimiento es una variable uniforme que se pueda usar como tiempo. Declarar uniforme en el sombreador de vértices:
uniform float uTime;
Ahora, para pasarlo al sombreador,
regresemos a
Water.js y definamos la variable de tiempo en initialize:
Water.prototype.initialize = function() { this.time = 0;
Ahora, para transferir la variable al sombreador, usamos
material.setParameter
. Primero, establecemos el valor inicial al final de la función
CreateWaterMaterial
:
Ahora, en la función de
update
, podemos realizar un incremento de tiempo y acceder al material usando el enlace creado para esto:
this.time += 0.1; this.material.setParameter('uTime',this.time);
Finalmente, en la función de intercambio, copiamos el valor anterior para que, incluso después de cambiar el código, continúe aumentando sin restablecer a 0.
Water.prototype.swap = function(old) { this.time = old.time; };
Ahora todo está listo. Ejecute el juego para asegurarse de que no haya errores. Ahora
Water.vert
nuestro avión usando la función de tiempo en
Water.vert
:
pos.y += cos(uTime)
¡Y nuestro avión debería comenzar a moverse hacia arriba y hacia abajo! Como ahora tenemos una función de intercambio, también podemos actualizar Water.js sin tener que reiniciar. Para asegurarse de que esto funciona, intente cambiar el incremento de tiempo.
Tarea 4: ¿puedes mover los vértices para que se vean como las ondas en la figura a continuación?
Déjame decirte que examiné en detalle el tema de varias formas de crear olas
aquí . El artículo está relacionado con 2D, pero los cálculos matemáticos son aplicables a nuestro caso. Si solo quiere ver la solución,
aquí está la esencia .
Translucidez
El propósito de esta sección es crear una superficie de agua translúcida.
Puede notar que el color devuelto a Water.frag tiene un valor de canal alfa de 0.5, pero la superficie aún permanece opaca. En muchos casos, la transparencia aún se convierte en un problema no resuelto en los gráficos por computadora. Una forma económica de resolverlo es usar la mezcla.
Por lo general, antes de dibujar un píxel, verifica el valor en el
búfer de profundidad y lo compara con su propio valor de profundidad (su posición en el eje Z) para determinar si se vuelve a dibujar el píxel actual de la pantalla. Esto es lo que le permite renderizar la escena correctamente sin tener que ordenar los objetos de atrás hacia adelante.
Al mezclar, en lugar de simplemente rechazar el píxel o sobrescribir, podemos combinar el color del píxel ya representado (objetivo) con el píxel que vamos a dibujar (la fuente). Puede encontrar una lista de todas las funciones de mezcla disponibles en WebGL
aquí .
Para que el canal alfa funcione de acuerdo con nuestras expectativas, queremos que el color combinado del resultado sea una fuente multiplicada por un canal alfa más un píxel de destino multiplicado por uno menos alfa. En otras palabras, si alfa = 0.4, entonces el color final debe tener un valor:
finalColor = source * 0.4 + destination * 0.6;
En PlayCanvas, esta es la operación que
realiza pc.BLEND_NORMAL .
Para habilitarlo, simplemente configure la propiedad del material dentro de
CreateWaterMaterial
:
material.blendType = pc.BLEND_NORMAL;
Si ahora comienzas el juego, ¡el agua se volverá translúcida! Sin embargo, todavía es imperfecto. El problema surge cuando la superficie translúcida se superpone sobre sí misma, como se muestra a continuación.
Podemos eliminarlo usando
alfa para cobertura , una técnica de muestreo múltiple para transparencia, en lugar de combinar:
Pero solo está disponible en WebGL 2. En el resto del tutorial, en aras de la simplicidad, utilizaré la mezcla.
Para resumir
Configuramos el entorno y creamos una superficie translúcida del agua con ondas animadas del sombreador de vértices. En la segunda parte del tutorial, consideraremos la flotabilidad de los objetos, agregaremos líneas a la superficie del agua y crearemos líneas de espuma a lo largo de los límites de los objetos que se cruzan con la superficie.
En la tercera (última) parte, consideraremos la aplicación del efecto de posprocesamiento de las distorsiones subacuáticas y consideraremos ideas para una mejora adicional.
Código fuente
El proyecto PlayCanvas terminado se puede encontrar
aquí . Nuestro repositorio también tiene un
puerto de proyecto en Three.js .