webgl实现2D探照灯效果

308 阅读3分钟

效果展示

项目演示地址

该项目相比canvas和css实现

  • 性能方面较高
  • 容易实现的边缘模糊效果
  • 不得不说css(box-shadow)挺香的
  • 最重要的是可以实现更多有意思的效果,基于你的想象力

相关说明

  • 本项目使用gl-renderer作为webgl基础库来简化代码,关于webgl原生js编写请自行查阅资料
  • 借于GPU的并发性,主要利用shader的像素化处理来实现

一. 初始化canvas

      const canvasWidth = 1450;
      const canvasHeight = 828;
      const canvas = document.querySelector("canvas");

      canvas.width = canvasWidth;
      canvas.height = canvasHeight;

      //存储偏移量 计算探照灯坐标时使用
      const canvasOffsetX = canvas.offsetLeft;
      const canvasOffsetY = canvas.offsetTop;

二. 监听鼠标移动并记录坐标

      //初始化探照灯位置(纹理)
      let curCanvasPosPercent = {
        x: canvasWidth / 2,
        y: canvasHeight / 2,
      };

      //这边可以依据个人情况适当节流
      window.addEventListener("mousemove", (e) => {
        curCanvasPosPercent.x = Math.max(Math.min(e.pageX - canvasOffsetX, canvasWidth), 0);
        curCanvasPosPercent.y = canvasHeight - Math.max(Math.min(e.pageY - canvasOffsetY, canvasHeight), 0);
      });

这一步主要是为了记录探照点坐标,当然也可以不依据鼠标位置,根据js自动变换鼠标位置

三. 初始化Webgl

     (async function () {
        const renderer = new GlRenderer(canvas);
        //分别加载顶点着色器和片元着色器初始化 program
        const program = await renderer.load("fragmentShader.frag", ["vertexShader.frag"]);
        renderer.useProgram(program);

        //加载纹理
        const texture = await renderer.loadTexture("xiaowu.png");
        renderer.uniforms.tMap = texture;

		//将画布的大小传给shader 用于像素渲染计算
        renderer.uniforms.canvasWidth = canvasWidth;
        renderer.uniforms.canvasHeight = canvasHeight;

        //设置顶点数据  顶点索引 uv坐标
        renderer.setMeshData([
          {
            positions: [
              [-1, -1],
              [-1, 1],
              [1, 1],
              [1, -1],
            ],
            attributes: {
              uv: [
                [0, 0],
                [0, 1],
                [1, 1],
                [1, 0],
              ],
            },
            cells: [
              [0, 1, 2],
              [2, 0, 3],
            ],
          },
        ]);

        renderer.render();

        function update() {
          //在每一帧获取最新坐标
          renderer.uniforms.xPot = curCanvasPosPercent.x;
          renderer.uniforms.yPot = curCanvasPosPercent.y;
          requestAnimationFrame();
        }
        update();
      })();

这一步主要就是初始化webgl program 并且在每一帧更新探照灯的x,y坐标

关于gl-renderer的相关api自行查阅文档

四. 顶点着色器

//a_vertexPosition 是 GlRenderer 默认顶点变量
attribute vec2 a_vertexPosition;
attribute vec2 uv;

varying vec2 vUv;

void main(){
  vUv=uv;
  //vec2 -> vec4
  gl_Position=vec4(a_vertexPosition,1,1);
}

这一步主要就是接收js传过来的顶点坐标 并将纹理坐标传递给片元着色器

五. 片元着色器

#ifdef GL_ES
precision highp float;
#endif

uniform sampler2D tMap;
uniform float canvasWidth;
uniform float canvasHeight;
uniform float xPot;
uniform float yPot;
varying vec2 vUv;

void main(){
  //计算实际像素距离
  float d=distance(vec2(vUv.x*canvasWidth,vUv.y*canvasHeight),vec2(xPot*canvasWidth,yPot*canvasHeight));
  //平滑阶梯函数  分界部分产生模糊渐变 抗锯齿
  float smoothValue=smoothstep(d,d+30.0,250.0);
  
  vec4 color=texture2D(tMap,vUv);
  gl_FragColor.rgb=smoothValue*color.rgb;
  gl_FragColor.a=1.0;
}

核心代码分析:

  • 通过distance内置函数计算每个片元坐标相对于探照灯中心的的距离(注意:这里的坐标计算是用的像素坐标而不是纹理(0,1)坐标,因为纹理坐标如果canvas宽高不一致的话,产生的探照灯就是椭圆了)
  • smoothstep函数的介绍
  • smoothstep相比step(0,1)阶梯函数,两个值得中间多了过渡的部分,产生了非0,1值,我们就利用这个平滑特性,实现探照灯边缘模糊处理

六. 扩展

  • 通过了解探照灯的案例,我们可以利用webgl对图片处理的并发性,实现更多有意思的效果,并且这不会带来过多的性能压力。

完整代码

github:github.com/hyy126/shad…

如果觉得该作品对您有帮助的话,欢迎star