forthealllight / blog

📖我的博客,记录学习的一些笔记,如有喜欢,欢迎star
2.3k stars 225 forks source link

通过webgl中的纹理贴图来自定义图片间的转场效果 #55

Open forthealllight opened 4 years ago

forthealllight commented 4 years ago

通过webgl中的纹理贴图来自定义图片间的转场效果


    本章节建立我们基本上掌握了如何在2D或者3D平面中绘制图形的基础上(当然不了解2D和3D平面绘制图片也可以往下阅读),我们在2D平面中,可以用webgl去绘制我们所需要的图案,但实际中我们用到某个图形时不可能全部一一的去绘制,纹理贴图解决了这个问题,我们可以用已有的图片来填充webgl中的图形。

  • 纹理基础
  • 2D图形的纹理贴图
  • 多纹理单元的纹理贴图
  • 不同纹理间实现转场特效

这个系列的源码地址为:源码的地址为: https://github.com/forthealllight/webgl-demo

一、纹理基础

    在了解纹理贴图之前,我们先来了解一下预备知识,首先什么是纹理贴图或者说纹理映射呢?

    将一张图像贴到一个几何体的表面

    这就是纹理贴图的含义,这张图像我们就成为纹理,这个工作我们就称为纹理贴图,其本质就是提取图像中的颜色,然后对应的赋予给几何平面的某个位置,从而将图像在几何体表面完成渲染。

    这里从纹理(图像)到几何体表面之间有一个映射,为了了解这个映射是怎么发生的,我们必须了解一下纹理坐标和裁剪坐标。

裁剪坐标

    webgl中的裁剪面坐标如下所示:

Lark20191216-181530

    从上述裁剪坐标系统的示意图中我们可以看出,webgl中整个裁剪平面的中心坐标是(0,0),对于二维的裁剪平面而言其水平方向从(-1,0)到(1,0),其竖直方向从(0,-1)到(0,1).

也就是说其裁剪坐标任何方向的值在区间[-1,1]内。

注意一点:裁剪平面是决定如何映射到画布上,因为画布是二维的,因此裁剪平面也是二维的。webgl中z轴的坐标并不限制在(-1,1),可以为任何值

纹理坐标

    纹理坐标跟裁剪坐标不一样,其值不是从[-1,1]而是从[0,1]:

Lark20191230-195608

在贴图过程中,我们需要使裁剪坐标中的顶点坐标,与纹理坐标系统点一一对应。也就是如何截取纹理坐标中的纹理,贴到几何图形中。

需要注意的是,图片本身的坐标与纹理的坐标是左右相等,上下相反的,因此,存在一个上下方向的坐标变换。在webgl中的纹理 中,通过:

 gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,1)

来反转Y轴上的方向。

禁用Chrome的安全性检测

    本文的例子中,直接本地通过file文件打开html,如果在这个html文件中有ajax跨域请求,那么因为浏览器的安全性检测,会提示如下信息的错误:

Cross origin requests are only supported for HTTP....

当然如果启动一个本地server就不会有影响。这里有种懒人解决方案,就是通过禁用安全性检测的方法,以mac为例,从命令行窗口中启动chrome,启动命令为

open /Applications/Google\ Chrome.app --args --allow-file-access-from-files

此外,也可以安装http-server,快速在本地启一个server.

二、2D图形的纹理贴图

    下面我们以2D几何图形的纹理贴图来介绍一下,在webgl中如何实现纹理贴图。

(1).首先创建一个纹理缓冲

function createTexture (gl, filter, data, width, height) {
  let textureRef = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, textureRef);
  gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,1)
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);
  return textureRef;
}

    上述就是一个创建纹理缓存的例子,通过gl.createTexture()创建一个纹理缓存,并关联系统变量gl.TEXTURE_2D. 此外,因为在纹理的渲染中,在Y轴方向与图片是完全相反的,因此如果需要正相呈现纹理渲染结果,比如进行Y轴方向的反转,即gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL,1)。

    此外我们可以通过gl.texParameteri函数,指定当图片纹理大于渲染区域,或者图片纹理小于渲染区域时,如何去正确的渲染。

    最后通过:

gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, data);

    提取data中的元素保存到纹理缓存中,data可以是image,也可以是video,甚至是另一个canvas的渲染结果。也就是说,webgl的纹理贴图的源,不仅仅可以是图片,还可以是视频的一帧,甚至是另一个cavans

(2).加载图片使用纹理缓冲

 let image = new Image()
 image.src = './cubetexture.png'
 image.onload = function(){
    let textureRef  = createTexture(gl,gl.LINEAR,image1);
    gl.activeTexture(gl.TEXTURE0)
    gl.bindTexture(gl.TEXTURE_2D,textureRef)
    gl.uniform1i(u_Sampler, 0);  // texture unit 0
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  }

上述代码加载image图片,在onload的时候,通过之前定义的createTexture创建纹理,然后激活使用0号纹理单元,webgl可以同时支持多个纹理单元,可以同时将多个纹理渲染到同一个可视区。激活纹理单元后,我们将0传递给着色器中的取样器变量u_Sampler.

最后我们在片元着色器中接受纹理单元编号,以及纹理坐标,最后贴图渲染到渲染区:

    uniform sampler2D u_Sampler; //取色器变量
    varying lowp vec2 v_TexCoord; //纹理坐标
    void main() {
      gl_FragColor = texture2D(u_Sampler,v_TexCoord); 
    }

我们最终得到了在所指定的区域,渲染了正确的一张图片,渲染结果如下所示:

Lark20200426-211044

三、多纹理单元的纹理贴图

    前面说到webgl可以同时支持多个纹理单元,webgl可以同时处理多幅纹理,纹理单元就是为了这个目的而设计的。在上述的例子中,我们只用了一个纹理单元,将一张纹理渲染到了渲染区。接下来我们尝试使用两个纹理单元,将两张图片,渲染到同一个区域。


纹理图片1

纹理图片2

    我们使用webgl中国的两个纹理单元,分别为gl.TEXTURE0和gl.TEXTURE1,修改代码如下。

首先是加载和渲染的逻辑:

  let textures = []
  image.onload = function(){
     let textureRef  = createTexture(gl,gl.LINEAR,image);
     gl.activeTexture(gl.TEXTURE0);
     gl.bindTexture(gl.TEXTURE_2D,textureRef)
     textures.push(textureRef)    
  }
  image.src = './cubetexture.png'
  let image1 = new Image();
  image1.onload = function(){
    let textureRef1  = createTexture(gl,gl.LINEAR,image1);
    gl.activeTexture(gl.TEXTURE1)
    gl.bindTexture(gl.TEXTURE_2D,textureRef1)
    textures.push(textureRef1)
  }
  image1.src = './img.png'

  //激活纹理单元并且绘制
  gl.uniform1i(u_Sampler, 0);  // texture unit 0
  gl.uniform1i(u_Sampler1, 1);  // texture unit 1
  gl.activeTexture(gl.TEXTURE0);
  gl.bindTexture(gl.TEXTURE_2D, textures[0]);
  gl.activeTexture(gl.TEXTURE1);
  gl.bindTexture(gl.TEXTURE_2D, textures[1]);

最后修改片元着色器:

uniform sampler2D u_Sampler;
uniform sampler2D u_Sampler1;
void main(){
  gl_FragColor = texture2D(u_Sampler,v_TexCoord) + texture2D(u_Sampler1,v_TexCoord); 
}

最后的渲染结果为:


最后双纹理单元合成渲染结果

完成的代码 地址为:https://github.com/forthealllight/webgl-demo/tree/master/demo13

四、不同纹理间实现转场效果

    通过使用多纹理单元,我们可以实现图片间的转场效果,首先我们来看什么是转场效果。

    转场效果顾名思义,就是从图片A切换到图片B之间的动画特效,类似与我们在做PPT的时候,一些淡入淡出等等动效。在webgl中,渲染的纹理本质也是图片,因此可以在不同纹理间,按时间顺序控制不同纹理的渲染结果,可以实现转场的效果。

如上所示的动图就是列出了一些转场特效。

    我们以同样前面的两张图片为例,研究一下图片或者说纹理间的转场效果。

直接看片元着色器中的代码:

vec4 transition (vec2 uv) {
  float time = progress;
  float stime = sin(time * PI / 2.);
  float phase = time * PI * bounces;
  float y = (abs(cos(phase))) * (1.0 - stime);
  float d = uv.y - y;
  return mix(
    mix(
      getToColor(uv),
      shadow_colour,
      step(d, shadow_height) * (1. - mix(
        ((d / shadow_height) * shadow_colour.a) + (1.0 - shadow_colour.a),
        1.0,
        smoothstep(0.95, 1., progress) // fade-out the shadow at the end
      ))
    ),
    getFromColor(vec2(uv.x, uv.y + (1.0 - y))),
    step(d, 0.0)
  );
}

    我们看到这个transition函数就是一个转换函数,决定了随着时间,如何切换纹理,从而进行渲染,我们通过getFromColor和getToColor指定了两个不同纹理单元的纹理:

    vec4 getToColor(vec2  uv){
      return texture2D(u_Sampler,uv);
    }
    vec4 getFromColor(vec2 uv){
      return texture2D(u_Sampler1,uv);
    }

最后片元着色器的颜色就是调用transition函数后的结果,在调用transition的时候我们传入了纹理坐标。

void main() {
    gl_FragColor =  transition(v_TexCoord);
}

最后要想要渲染结果动起来,就必须写一个定时器,动态改变transition中的参数progress,是其从0到1的变化。


setInterval(()=>{
    if(textures.length === 2){
      if(i >= 1){
        i = 0.01
      }
      gl.uniform1i(u_Sampler, 0);  // texture unit 0
      gl.uniform1i(u_Sampler1, 1);  // texture unit 1
      gl.activeTexture(gl.TEXTURE0);
      gl.bindTexture(gl.TEXTURE_2D, textures[0]);
      gl.activeTexture(gl.TEXTURE1);
      gl.bindTexture(gl.TEXTURE_2D, textures[1]);
      let progress = gl.getUniformLocation(shaderProgram,'progress')
      gl.uniform1f(progress,i)
      i += 0.05
      gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

    }
  },100)

最后的渲染结果我们可以看到的动画结果如下:

Untitled1

完成的代码 地址为:https://github.com/forthealllight/webgl-demo/tree/master/demo12

此外,在https://github.com/gl-transitions/gl-transitions 上收录了各色各样的转场效果,转场不仅仅可以应用于图片,还可以应用于视频的不同帧间,下篇文章将具体讲讲如何实现在webgl中渲染video,以及video的帧间动画。

syd192 commented 4 years ago

下篇文章,有空记得更呀

forthealllight commented 4 years ago

下篇文章,有空记得更呀

好的