En la
segunda parte, examinamos la flotabilidad y las líneas de espuma. En esta última parte, aplicamos distorsión subacuática como efecto de postprocesamiento.
Efectos de refracción y postprocesamiento
Nuestro objetivo es transmitir visualmente la refracción de la luz en el agua. Ya
hablamos sobre cómo crear este tipo de distorsión en un sombreador de fragmentos para una escena 2D. Aquí la única diferencia es que necesitamos entender qué área de la pantalla está debajo del agua y aplicar distorsión solo a ella.
Post procesamiento
En el caso general, el efecto de posprocesamiento es cualquier efecto aplicado a toda la escena después de su representación, por ejemplo, sombras de color o el
efecto de una pantalla CRT antigua . En lugar de renderizar la escena directamente en la pantalla, primero la renderizamos al búfer o textura, y luego, pasando la escena a través de nuestro sombreador, la renderizamos a la pantalla.
En PlayCanvas, puede personalizar este efecto de postprocesamiento creando un nuevo script. Llamémoslo
Refraction.js y copiemos esta plantilla en blanco:
Esto es similar a un script normal, pero definimos una clase
RefractionPostEffect
que se puede aplicar a la cámara. Para renderizar, necesita sombreadores de vértices y fragmentos. Los atributos ya están configurados, así que
creemos Refraction.frag con los siguientes contenidos:
precision highp float; uniform sampler2D uColorBuffer; varying vec2 vUv0; void main() { vec4 color = texture2D(uColorBuffer, vUv0); gl_FragColor = color; }
Y
Refraction.vert con un sombreador de vértices básico:
attribute vec2 aPosition; varying vec2 vUv0; void main(void) { gl_Position = vec4(aPosition, 0.0, 1.0); vUv0 = (aPosition.xy + 1.0) * 0.5; }
Ahora adjunte el script
Refraction.js a la cámara y asigne los atributos apropiados a los sombreadores. Cuando comiences el juego, verás la escena de la misma manera que antes. Este es un efecto posterior vacío que simplemente vuelve a representar la escena. Para asegurarnos de que funciona, intentemos darle a la escena un tinte rojo.
En lugar de simplemente devolver el color a Refraction.frag, intente configurar el componente rojo en 1.0, lo que debería dar a la imagen la imagen que se muestra a continuación.
Sombreador de distorsión
Para crear una distorsión animada, necesitamos agregar una variable de tiempo uniforme, así que creémosla dentro de este constructor de post-efecto en Refraction.js:
var RefractionPostEffect = function (graphicsDevice, vs, fs) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs;
Ahora dentro de la función de renderizado, lo pasamos a nuestro sombreador para aumentarlo:
RefractionPostEffect.prototype = pc.extend(RefractionPostEffect.prototype, {
Ahora podemos usar el mismo código de sombreador del tutorial de distorsión del agua, convirtiendo nuestro sombreador de fragmentos completos en lo siguiente:
precision highp float; uniform sampler2D uColorBuffer; uniform float uTime; varying vec2 vUv0; void main() { vec2 pos = vUv0; float X = pos.x*15.+uTime*0.5; float Y = pos.y*15.+uTime*0.5; pos.y += cos(X+Y)*0.01*cos(Y); pos.x += sin(XY)*0.01*sin(Y); vec4 color = texture2D(uColorBuffer, pos); gl_FragColor = color; }
Si todo se hace correctamente, la imagen completa debería verse como si estuviera completamente bajo el agua.
Tarea 1: asegúrese de que la distorsión solo se aplique a la parte inferior de la pantalla.
Máscaras de cámara
Ya casi hemos terminado. Nos queda aplicar este efecto de distorsión a la parte subacuática de la pantalla. La forma más fácil en la que pensé es volver a renderizar la escena con la superficie del agua en blanco sólido, como se muestra en la figura a continuación.
Se convertirá en la textura que usamos como máscara. Luego, transferiremos esta textura a nuestro sombreador de refracción, que distorsionará el píxel en la imagen terminada solo cuando el píxel correspondiente en la máscara sea blanco.
Agreguemos un atributo booleano a la superficie del agua para saber si se usa como máscara. Agregue lo siguiente a Water.js:
Water.attributes.add('isMask', {type:'boolean',title:"Is Mask?"});
Luego, como de costumbre, podemos pasarlo al sombreador usando
material.setParameter('isMask',this.isMask);
. Luego declararlo en Water.frag y colorear el píxel blanco si el atributo es verdadero.
Asegúrese de que esto funcione activando la propiedad "¿Es la máscara?". en el editor y reiniciando el juego. Debería verse blanco, como en la imagen de arriba.
Ahora, para volver a renderizar la escena, necesitamos una segunda cámara. Cree una nueva cámara en el editor y
llámela CameraMask . También duplicamos la entidad Water en el editor y
nombramos la
WaterMask duplicada. Asegúrese de la entidad "¿El agua es la máscara?" es falso y WaterMask es verdadero.
Para ordenar una nueva cámara para renderizar a una textura en lugar de a una pantalla, cree un nuevo script
CameraMask.js y adjúntelo a la nueva cámara. Creamos un
RenderTarget para capturar la salida de esta cámara:
Ahora, después de iniciar la aplicación, verá que esta cámara ya no se muestra en la pantalla. Podemos obtener la salida de su renderizado objetivo en
Refraction.js de la siguiente manera:
Refraction.prototype.initialize = function() { var cameraMask = this.app.root.findByName('CameraMask'); var maskBuffer = cameraMask.camera.renderTarget.colorBuffer; var effect = new pc.RefractionPostEffect(this.app.graphicsDevice, this.vs.resource, this.fs.resource, maskBuffer);
Observe que paso esta textura de máscara como argumento al constructor de post-efecto. Necesitamos crear un enlace a él en nuestro constructor, por lo que se verá así:
Finalmente, en la función de renderizado, pasamos el buffer a nuestro sombreador:
scope.resolve("uMaskBuffer").setValue(this.buffer);
Ahora, para asegurarme de que todo esto funcione, te lo dejaré como una tarea.
Tarea 2: renderice el uMaskBuffer en la pantalla para asegurarse de que sea la salida de la segunda cámara.
Debe considerarse lo siguiente: el renderizado de destino está configurado en la inicialización del script CameraMask.js, y debe estar listo para cuando se llame a Refraction.js. Si los scripts funcionan de manera diferente, obtenemos un error. Para asegurarse de que funcionan en el orden correcto, arrastre CameraMask a la parte superior de la lista de entidades en el editor, como se muestra a continuación.
La segunda cámara siempre debe verse con la misma vista que la original, así que hagamos que siempre siga la posición y la rotación del script CameraMask.js en la actualización:
CameraMask.prototype.update = function(dt) { var pos = this.CameraToFollow.getPosition(); var rot = this.CameraToFollow.getRotation(); this.entity.setPosition(pos.x,pos.y,pos.z); this.entity.setRotation(rot); };
En inicializar, defina
CameraToFollow
:
this.CameraToFollow = this.app.root.findByName('Camera');
Máscaras de recorte
Ambas cámaras ahora representan lo mismo. Queremos que la cámara con máscara represente todo excepto el agua real, y la cámara real represente todo excepto el agua con máscara.
Para hacer esto, podemos usar la máscara de recorte de bits de la cámara. Funciona de manera similar
a las máscaras de colisión . Se recortará un objeto (es decir, no se procesará) si el resultado de
AND
bit a bit entre su máscara y la máscara de la cámara es 1.
Suponga que Water tiene el bit 2, WaterMask tiene el bit 3. Todos los bits, excepto el 3, deben configurarse para una cámara real, y todos los bits excepto el 2 para una cámara con máscara. La forma más fácil de decir "todos los bits excepto N" es la siguiente manera:
~(1 << N) >>> 0
Lea más sobre operaciones bit a bit
aquí .
Para configurar las máscaras de recorte de la cámara, podemos insertar lo siguiente en la parte inferior de la inicialización del script
CameraMask.js :
Ahora en Water.js estableceremos el bit 2 de la máscara de la malla de agua, y la versión de la máscara en el bit 3:
Ahora una especie estará con agua corriente y la segunda con agua blanca sólida. La imagen a la izquierda muestra la vista desde la cámara original y a la derecha la vista desde la cámara con máscara.
Aplicación de máscara
Y ahora el último paso! Sabemos que las áreas submarinas están marcadas con píxeles blancos. Solo necesitamos verificar si estamos en un píxel blanco, y si no, apagar la distorsión en
Refraction.frag :
¡Y esto debería resolver nuestro problema!
También vale la pena señalar que, dado que la textura de la máscara se inicializa en el inicio, cuando cambia el tamaño de la ventana en tiempo de ejecución, ya no corresponderá al tamaño de la pantalla.Suavizado
Puede notar que los bordes de la escena ahora se ven un poco nítidos. Esto sucedió porque después de aplicar el efecto posterior, perdimos suavizado.
Podemos aplicar suavizado adicional sobre nuestro efecto como otro efecto posterior. Afortunadamente, hay otra variable en la
tienda PlayCanvas que podemos usar. Vaya a la
página de recursos del
script , haga clic en el gran botón verde de descarga y seleccione su proyecto de la lista que aparece. El script aparecerá en la raíz de la ventana Activos como
posteffect-fxaa.js . ¡Solo conéctelo a la entidad de la cámara y su escena comenzará a verse mucho mejor!
Pensamientos en conclusión
¡Si llegas aquí, puedes elogiarte! En este tutorial cubrimos bastantes técnicas. Ahora debe tener confianza cuando trabaje con sombreadores de vértices, renderice texturas, aplique efectos de posprocesamiento, recorte selectivo de objetos, utilice el búfer de profundidad y trabaje con fusión y transparencia. Aunque hemos implementado todo esto en PlayCanvas, puede conocer todos estos conceptos generales de gráficos por computadora de una forma u otra en cualquier plataforma.
Todas estas técnicas también son aplicables a muchos otros efectos. Una aplicación particularmente interesante encontrada para sombreadores de vértices, la encontré en el
informe en el gráfico de Abzu , donde los desarrolladores explican cómo usaron sombreadores de vértices para animar efectivamente decenas de miles de peces en la pantalla.
¡Ahora tienes un hermoso efecto de agua que puedes aplicar en tus juegos! Puede personalizarlo y agregar sus propios detalles. Se puede hacer mucho más con agua (ni siquiera mencioné ninguno de los tipos de reflejos). A continuación hay un par de ideas.
Ondas de ruido
En lugar de solo animar las olas con una combinación de cosenos y senos, puede muestrear la textura para que las olas se vean un poco más naturales y menos predecibles.
Rastros dinámicos de espuma
En lugar de líneas de agua completamente estáticas en la superficie, puede dibujar la textura al mover objetos para crear trazas dinámicas de espuma. Esto se puede hacer de muchas maneras diferentes, por lo que esta tarea en sí misma puede convertirse en un proyecto.
Código fuente
El proyecto PlayCanvas terminado se puede encontrar
aquí . Nuestro repositorio también tiene un
puerto de proyecto en Three.js .