ddfssa

Tutoriales: manipulación de fragmentos

Creando un efecto líquido simple en una textura usando WebGL

Haga click en la imagen de abajo para crear una ondulación.





Si quiere probarlo, aquí está el link de descarga de la carpeta .zip que contiene los archivos que hacen funcionar este ejemplo. Los archivos contenidos en esta carpeta son:

  • Common (una carpeta con archivos de configuración de WebGL)
  • backGLiquid.png (la imagen de fondo)
  • index.html (el archivo de HTML principal, aquí se encuentran los shaders)
  • principal.js (el archivo de JavaScript principal)
  • README.txt (información acerca de los autores)
  • squareSimple.obj (un archivo OBJ que contiene el cuadro usado)
                
    Contenido:           
    
    - Configuración
    - El loop principal
    - La función render
    - El shader de vértices
    - El shader de fragmentos
    
            

Las partes más importantes que hacen posible este ejemplo de WebGL son: dibujar un cuadrado simple donde se va a dibujar la textura y escribir un shader de fragmentos que modifique las coordenadas UV que se utilizan para muestrear la textura, de esta manera creando el efecto deseado.

Configuración:

En primer lugar, empezamos por establecer una variable global denominada canvas, esta se va a utilizar para hacer referencia al elemento Canvas en el que dibujaremos. También, se establece explícitamente que el contenido del Canvas cambiará constantemente, 60 veces por segundo idealmente, debido al hecho de que requestAnimationFrame se utiliza.

                
    canvas = document.getElementById( "gl-canvas" );
    canvas.style.willChange = 'contents';
            

Se declaran las variables que indicarán la posición del mouse relativa al Canvas.

                
    rect = canvas.getBoundingClientRect();
    rectLeft = rect.left - window.scrollX;
    rectTop = rect.top + window.scrollY;
    rectLeftA = rectLeft;
    rectTopA = rectTop;
            

Una variable llamada gl nos dará el contexto de WebGL. Se utiliza un elemento Canvas de HTML, si el navegador no permite el uso de WebGL se muestra un mensaje. Además, la ventana de visualización de WebGL se configura para tener el mismo tamaño que el Canvas.

                
    gl = WebGLUtils.setupWebGL( canvas );
    if ( !gl ) { alert( "WebGL isn't available." ); }
    
    gl.viewport( 0, 0, canvas.width, canvas.height );
            

Se crea un buffer de vértices y de índices y estos se cargan con la información contenida en verSquareSimple e indSquareSimple.

                
    bufVSquareSimple = gl.createBuffer();
    gl.bindBuffer( gl.ARRAY_BUFFER, bufVSquareSimple );
    gl.bufferData( gl.ARRAY_BUFFER,verSquareSimple, gl.STATIC_DRAW );
    
    bufISquareSimple = gl.createBuffer();
    gl.bindBuffer( gl.ELEMENT_ARRAY_BUFFER, bufISquareSimple );
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,indSquareSimple, gl.STATIC_DRAW);
            

Configurando el shader de vértices y el shader de fragmentos que se va a utilizar.

                
    proSquareSimple = initShaders( gl, "progSquareSimpleVer", "progSquareSimpleFra" );
            

Se obtiene la ubicación de los atributos de los vértices, específicamente los atributos identificados como "vértices" y "uvs" que se utilizan en el shader.

                
    vLSquareSimple = gl.getAttribLocation( proSquareSimple, "vertices" );
    gl.enableVertexAttribArray( vLSquareSimple );
    cLSquareSimple = gl.getAttribLocation( proSquareSimple, "uvs" );
    gl.enableVertexAttribArray( cLSquareSimple );
            

Se obtiene la ubicación de las uniformes que se utilizan en el shader de fragmentos.

                
    fUSquareSimple0 = gl.getUniformLocation( proSquareSimple, "pos" );
    fUSquareSimple1 = gl.getUniformLocation( proSquareSimple, "time" );
    fUSquareSimple2 = gl.getUniformLocation( proSquareSimple, "timeA" );
    fUSquareSimple3 = gl.getUniformLocation( proSquareSimple, "propa" );
    
    texSquareSimpleL = gl.getUniformLocation( proSquareSimple, "texture" );
            

Se necesita cargar la imagen que va a ser utilizada como la textura en el cuadrado.

                
    images[0].src = imaDisp[0];
    images[0].onload = function() { configureTexture( images[0],0);};
            

La función llamada configureTexture crea el elemento de textura de WebGL, la imagen tiene información en RGBA. Un filtro lineal se utiliza cuando se procesa la textura. Además, la variable loader cuenta cada vez que se carga la imagen, esta variable se utilizará para saber si la imagen se ha cargado correctamente antes de utilizarla.

                
    function configureTexture(image, index) {
    
        textures[index] = gl.createTexture();
    
        gl.bindTexture( gl.TEXTURE_2D, textures[index] );
        gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
    
        gl.texImage2D( gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image );
    
        gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER,  gl.LINEAR);
        gl.texParameteri( gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
    
        loader++;
    
    }
            

Por último, algunos “eventListeners” se establecen, estos se disparan cuando cierto evento es detectado. La función scrollFunc se utiliza para actualizar la posición del cursor cuando se desplaza la página. La función mouseMoved actualiza la posición del cursor cuando este se mueve dentro del Canvas. Tanto clickUpCanvas como clickDownCanvas se utilizan para comprobar si se hizo click dentro del Canvas.

                
    document.addEventListener("scroll", scrollFunc);
    
    canvas.addEventListener("mousemove", mouseMoved);
    canvas.addEventListener("mouseup", clickUpCanvas);
    canvas.addEventListener("mousedown", clickDownCanvas);
            

Lo que ocurre dentro de la función scrollFunc es que las variables rectLeft y rectTop se actualizan utilizando los valores actuales de window.scrollX y window.scrollY de la página.

                
    function scrollFunc(event){
        var wSY = window.scrollY;
        rectLeft = rectLeftA - window.scrollX;
        rectTop = rectTopA - wSY;
    }
            

La función mouseMoved es la función que actualiza los valores de estas dos variables globales: mouseX y mouseY. Estas variables son muy importantes ya que se utilizan para saber de dónde, en el Canvas, va a partir de la ondulación.

                
    function mouseMoved(event){
        mouseX = event.clientX  - rectLeft;
        mouseY = event.clientY  - rectTop;
    }
            

El loop principal:

Esta función es llamada constantemente para lograr el efecto de animación, esto gracias a que se usa requestAnimationFrame. La variable global time aumenta en uno cada vez que esta función es invocada, esta variable se utiliza como temporizador. Revisamos el valor de la variable loader para asegurarnos de que la textura se cargue antes de llamar a la función render.

                
    function loop() {

        time++;    

        if(loader===1){
            render();
        }

        requestAnimationFrame(loop);

    }
            

La función render:

En esta función hacemos todo lo relacionado con el contexto de WebGL. En primer lugar, se limpia del buffer de color.

                
    gl.clear(gl.COLOR_BUFFER_BIT);
            

Configurando el programa para usar proSquareSimple.

                
    gl.useProgram(proSquareSimple);
            

Se vincula el buffer de vértices para luego establecer los punteros de los atributos que van a ser utilizados en este dibujado, los punteros corresponden a "vértices" y "uvs", estos se usan en el shader de vértices.

                
    gl.bindBuffer( gl.ARRAY_BUFFER, bufVSquareSimple );
    gl.vertexAttribPointer( vLSquareSimple, 3, gl.FLOAT, false, 4*(8), 0 );
    gl.vertexAttribPointer( cLSquareSimple, 2, gl.FLOAT, false, 4*(8), 4*3 );
            

Configurando la textura.

                
    gl.uniform1i(texSquareSimpleL, 0);
    gl.activeTexture(gl.TEXTURE0);
    gl.bindTexture(gl.TEXTURE_2D, textures[0]);
            

La cantidad máxima de ondulaciones que se pueden crear son 15, es por esto que el bucle “for” cuenta hasta este número. Las variables timeA y propa son matrices de longitud 15 que contienen información específica para cada ondulación. La variable timeA se utiliza como un temporizador y también como el límite de la ondulación, cuando timeA llega a 1000, la ondulación se detiene por completo. Esa es la cantidad de fotogramas que marcan el límite del dibujado de una ondulación, entonces, si son 1000 fotogramas a 60 fotogramas por segundo esto significa que una ondulación existe durante 16.67 segundos. La variable propa indica el límite del radio de cada ondulación, se hace más grande a medida que pasa el tiempo para crear el efecto de propagación desde un punto central.

                
    for (var i = 0; i < 15; i++) {
        if(timeA[i]<1000){
            timeA[i] += 0.1+(timeA[i]/250); 
            propa[i] += 0.005;
        }
    }
            

Se envían los valores de las uniformes al GPU. La variable time es común para todas las ondulaciones. Las variables posLi, timeA y propa son específicas para cada una.

                
    gl.uniform2fv(fUSquareSimple0, posLi);
    
    gl.uniform1f(fUSquareSimple1, time/3);

    gl.uniform1fv(fUSquareSimple2, timeA);

    gl.uniform1fv(fUSquareSimple3, propa);
            

Finalmente, vinculamos el buffer de índices y dibujamos.

                
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, bufISquareSimple);

    gl.drawElements(4, indSquareSimple.length, gl.UNSIGNED_SHORT, 0);
            

La función clickUpCanvas se dispara cuando se hace click en el Canvas. La variable indexLi es la que controla el puntero en la arreglo. En primer lugar, se comprueba el valor de la variable timeA para ver si el valor es superior a 50, esto se hace para crear un espacio de tiempo entre la creación de una nueva ondulación y la última que se creó. La variable posLi almacena el valor de la posición del mouse cuando se crea una ondulación, éste es el punto central desde donde se propaga la ondulación. Ambos timeA y propa se inicializan e indexLi aumenta en uno para mover el puntero de los arreglos, también, cuando el valor de indexLi es 15 el valor se pone de nuevo en 0 para reiniciar el puntero.

                
    function clickUpCanvas(event){
        event.preventDefault();
        if(timeA[indexLi]>50){
            posLi[indexLi*2+0] = mouseX/512;
            posLi[indexLi*2+1] = 1-(mouseY/512);
            timeA[indexLi] = 0.01;
            propa[indexLi] = 0.01;
            indexLi++;
            if(indexLi===15){
                indexLi = 0;
            }
        }
    }
            

El shader de vértices:

El shader de vértices, en este caso, es un programa muy simple y corto, toma la posición de los vértices y los pasa sin ninguna modificación a gl_Positon. También, toma el atributo uvs y lo pasa al shader de fragmentos con la ayuda del registro variable llamado uvsv, esta información se usa para muestrear la textura.

                
    attribute vec4 vertices;
    attribute vec2 uvs;

    varying vec2 uvsv;

    void main()
    {   
        gl_Position = vertices;
        uvsv = uvs;
    }
            

El shader de fragmentos:

El shader de fragmentos se encarga de la parte más importante de este ejemplo, en este programa usamos algunos registros uniformes para calcular los nuevos valores que se usarán cuando se haya que muestrear la textura. Primero, establecemos la precisión en mediump, esta es la precisión de las variables de tipo float. El registro variable uvsv es el que tiene la información original que se utiliza para calcular la textura, éste es el que se modifica. Luego, declaramos un uniforme que tiene la información de la textura.

                
    precision mediump float;
    varying vec2 uvsv;
    uniform sampler2D texture;
            

El registro uniforme llamado pos es un arreglo de longitud 15 que tiene la posición del centro de la ondulación, es importante notar que es un arreglo de vec2, lo que significa que tiene dos variables de tipo float para cada posición en el arreglo, esto es porque tiene las coordenadas X e Y del punto.

                
    uniform vec2 pos[15];
            

Se necesitan otros tres uniformes, time es una variable de tipo float, mientras que timeA y propa son ambos arreglos de tipo float de longitud 15.

                
    uniform float time;
    uniform float timeA[15];
    uniform float propa[15];
            

Se declara dista, una variable de tipo float, uvsvA es un vec2 que contiene la misma información que uvsv, esto se hace porque no podemos modificar el registro variable uvsv directamente, por lo que usamos uvsvA en su lugar, esta es la variable que realmente se modifica.

                
    float dista;
    vec2 uvsvA = uvsv;
            

El bucle “for” cuenta hasta 15 porque ese es el límite de ondulaciones que se pueden crear.

                
    for(int i =0; i<15; i++){
        if(timeA[i]<1000.0){
            dista = distance(pos[i].xy, uvsvA.xy);
            if(dista<propa[i]){

                float difX = uvsv.x-pos[i].x;
                float difY = uvsv.y-pos[i].y;

                float fact = 0.3*(dista/timeA[i])*cos(5.0*(3.1416/2.0)*(dista/0.09) - time);

                uvsvA.x -= fact*(difX/dista);
                uvsvA.y -= fact*(difY/dista);
            }
        }
    }
            

De forma similar a la función render en el archivo de JavaScript, timeA tiene un límite de 1000 fotogramas, cuando timeA alcanza ese número se deja de dibujar la ondulación.

                
    for(int i =0; i<15; i++){
        if(timeA[i]<1000.0){
            dista = distance(pos[i].xy, uvsvA.xy);
            if(dista<propa[i]){

                float difX = uvsv.x-pos[i].x;
                float difY = uvsv.y-pos[i].y;

                float fact = 0.3*(dista/timeA[i])*cos(5.0*(3.1416/2.0)*(dista/0.09) - time);

                uvsvA.x -= fact*(difX/dista);
                uvsvA.y -= fact*(difY/dista);
            }
        }
    }
            

La variable dista almacena la distancia entre el centro de la ondulación, dado por pos, y un punto en la textura, dado por uvsvA. Entonces, esta distancia se compara con el valor del uniforme llamado propa, si la distancia es mayor que el radio de propagación definido por propa, la ondulación no se toma en cuenta. Las variables difX y difY son las componentes de un vector que apunta desde el centro hasta el punto en la textura que se analiza en ese momento. La variable llamada fact es la más importante aquí, describe por cuando el punto original que se iba a muestrear tiene que ser modificado. Después de calcular esto, le restaremos este valor al punto original, almacenado en uvsvA, para obtener el nuevo punto, este nuevo punto tiene la dirección calculada con anterioridad.

                
    for(int i =0; i<15; i++){
        if(timeA[i]<1000.0){
            dista = distance(pos[i].xy, uvsvA.xy);
            if(dista<propa[i]){

                float difX = uvsv.x-pos[i].x;
                float difY = uvsv.y-pos[i].y;

                float fact = 0.3*(dista/timeA[i])*cos(5.0*(3.1416/2.0)*(dista/0.09) - time);

                uvsvA.x -= fact*(difX/dista);
                uvsvA.y -= fact*(difY/dista);
            }
        }
    }
            

Finalmente, obtenemos el valor del color resultante del muestreo de la textura con el nuevo valor de uvsvA. Este valor se pasa a gl_FragColor.

                
    vec4 color = texture2D( texture, uvsvA );
    gl_FragColor = color;
            



Esos son todos los detalles importantes que hacen que este experimento funcione. Espero que ahora sepa cómo crear este tipo de efecto que aprovecha la potencia que el shader de fragmentos proporciona. También, espero que este ejemplo le dé una mejor comprensión de cómo usar WebGL en general.

Asegúrese de probarlo por sí mismo usando el enlace de descarga para los archivos proporcionado al principio. Le invito a cambiar algunos valores en el código para ver qué sucede, puede intentar cambiar la velocidad de propagación de las ondulaciones, tal vez cambiar la cantidad de ondulaciones que se pueden crear de una sola vez o cambiar la cantidad de tiempo que tardan las ondulaciones en desaparecer.

Siga experimentando y gracias por leer.



⇣ Más contenido ⇣


Juegos:

Tanque 3D: Sports

◾ Juega Tanque 3D: Sports

Tanque 3D: Tank Battle

◾ Juega Tanque 3D: Tank Battle

Robotic Sports: Tennis

◾ Juega Robotic Sports: Tennis


Tutoriales:

Manipulación de vértices

◾ Creando un efecto líquido simple en una malla usando WebGL

Iluminación

◾ Iluminando una escena con mapeo de sombras usando WebGL




⇡ Más contenido ⇡