Crea un sombreador de agua de dibujos animados para la web. Parte 3

imagen

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:

//---------------   ------------------------// pc.extend(pc, function () { //  -      var RefractionPostEffect = function (graphicsDevice, vs, fs, buffer) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs; //      this.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); this.buffer = buffer; }; //      pc.PostEffect RefractionPostEffect = pc.inherits(RefractionPostEffect, pc.PostEffect); RefractionPostEffect.prototype = pc.extend(RefractionPostEffect.prototype, { //      render, //    ,    , //       render: function (inputTarget, outputTarget, rect) { var device = this.device; var scope = device.scope; //       .  ,     scope.resolve("uColorBuffer").setValue(inputTarget.colorBuffer); //       .       . //          pc.drawFullscreenQuad(device, outputTarget, this.vertexBuffer, this.shader, rect); } }); return { RefractionPostEffect: RefractionPostEffect }; }()); //---------------  ------------------------// var Refraction = pc.createScript('refraction'); Refraction.attributes.add('vs', { type: 'asset', assetType: 'shader', title: 'Vertex Shader' }); Refraction.attributes.add('fs', { type: 'asset', assetType: 'shader', title: 'Fragment Shader' }); //  initialize       Refraction.prototype.initialize = function() { var effect = new pc.RefractionPostEffect(this.app.graphicsDevice, this.vs.resource, this.fs.resource); //     postEffects var queue = this.entity.camera.postEffects; queue.addEffect(effect); this.effect = effect; //       this.savedVS = this.vs.resource; this.savedFS = this.fs.resource; }; Refraction.prototype.update = function(){ if(this.savedFS != this.fs.resource || this.savedVS != this.vs.resource){ this.swap(this); } }; Refraction.prototype.swap = function(old){ this.entity.camera.postEffects.removeEffect(old.effect); this.initialize(); }; 

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; //       this.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); // >>>>>>>>>>>>>    this.time = 0; }; 

Ahora dentro de la función de renderizado, lo pasamos a nuestro sombreador para aumentarlo:

 RefractionPostEffect.prototype = pc.extend(RefractionPostEffect.prototype, { //      render, //      , //       render: function (inputTarget, outputTarget, rect) { var device = this.device; var scope = device.scope; //       .  ,     scope.resolve("uColorBuffer").setValue(inputTarget.colorBuffer); /// >>>>>>>>>>>>>>>>>>    uniform-  scope.resolve("uTime").setValue(this.time); this.time += 0.1; //       .       . //          pc.drawFullscreenQuad(device, outputTarget, this.vertexBuffer, this.shader, rect); } }); 

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.

 //    uniform uniform bool isMask; //      ,    //    true if(isMask){ color = vec4(1.0); } 

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:

 //  initialize       CameraMask.prototype.initialize = function() { //  512x512x24-      var colorBuffer = new pc.Texture(this.app.graphicsDevice, { width: 512, height: 512, format: pc.PIXELFORMAT_R8_G8_B8, autoMipmap: true }); colorBuffer.minFilter = pc.FILTER_LINEAR; colorBuffer.magFilter = pc.FILTER_LINEAR; var renderTarget = new pc.RenderTarget(this.app.graphicsDevice, colorBuffer, { depth: true }); this.entity.camera.renderTarget = renderTarget; }; 

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í:

 ////       var RefractionPostEffect = function (graphicsDevice, vs, fs, buffer) { var fragmentShader = "precision " + graphicsDevice.precision + " float;\n"; fragmentShader = fragmentShader + fs; //       this.shader = new pc.Shader(graphicsDevice, { attributes: { aPosition: pc.SEMANTIC_POSITION }, vshader: vs, fshader: fs }); this.time = 0; //// <<<<<<<<<<<<<    this.buffer = buffer; }; 

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 :

  //   ,  2 this.entity.camera.camera.cullingMask &= ~(1 << 2) >>> 0; //   ,  3 this.CameraToFollow.camera.camera.cullingMask &= ~(1 << 3) >>> 0; //      ,   : // console.log((this.CameraToFollow.camera.camera.cullingMask >>> 0).toString(2)); 

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:

 //      initialize  Water.js //    var bit = this.isMask ? 3 : 2; meshInstance.mask = 0; meshInstance.mask |= (1 << bit); 

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 :

 //   ,      vec4 maskColor = texture2D(uMaskBuffer, pos); vec4 maskColor2 = texture2D(uMaskBuffer, vUv0); //     ? if(maskColor != vec4(1.0) || maskColor2 != vec4(1.0)){ //      pos = vUv0; } 

¡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 .

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


All Articles