WebGL实现拖拽2D贴图的自转矩形

每个人的花期都不同,不用焦虑别人比你提前拥有。

写在最前

今天是六一儿童节,公司下班前在楼下举办了活动,我赶紧下楼活动了一下筋骨,别的不敢说,凑热闹这种事情我最在行了😄不过过节归过节,文还是要写的,开始吧。

捋捋逻辑

  1. 拖拽
  2. 贴图
  3. 自转
  4. 矩形

这几个功能在DOM操作上肯定是非常简单的,那遇到webGL该如何实现呢?

准备顶点数据

首先我们先完成绘制矩形这个功能:

遗憾的是drawArray并没有直接绘制矩形的API,它的做法是用两个三角形进行拼接形成矩形。

我们需要准备12个顶点。

-100, -100, 100, -100, -100, 100, 100, 100, 100, -100, -100, 100

1.png

由这6个点形成两个三角形也形成了矩形。细心的朋友可以看到P3、P6点 和 P2、P5点重合了,后续我们会以索引的方式进行改良,此文不详说。

转换坐标系

我们像往常一样先把基本的着色器写好。

  static VERTEX_SHADER: string = `
     attribute vec2 a_position;
     uniform vec2 u_translation;
     uniform vec2 u_resolution;
     attribute vec2 a_texCoord;
     varying vec2 v_texCoord;

     void main() {
        vec2 position =  (a_position + u_translation) / u_resolution * 2.0 - 1.0;
        gl_Position = vec4(position * vec2(1,-1), 0,1);
        v_texCoord = a_texCoord;
     }
  `;

仔细观察,我们会发现和昨天的着色器多了一些东西。

u_translation 位移常量

u_resolution 这里指的是屏幕宽高的变量

在上一篇文章中讲到,我们浏览器的坐标系和webGL坐标系并不一样。

2.png

如果想要使用习惯的浏览器坐标系,则需要将WebGL坐标系进行变换。

接下来举个例子,我们通常使用像素来表示位置(50,50)。

4.png

  1. 将500乘500的画布转为1乘1的坐标系,即该点需除以500,转换后它的位置为(0.1,0.1)。

  2. 浏览器坐标系是长度为1,而webGL为 -1到1长度为2 ,则扩大一倍 即 *2。

  3. 坐标系为0到2,若要变成-1到1,则坐标系往左移1,即 -1。

再然后因为WebGL坐标系Y轴与浏览器坐标轴相反,则需Y轴翻转,即 *-1。

由此得到以下程序完成坐标转换。

   vec2 position =  (a_position + u_translation) / u_resolution * 2.0 - 1.0;
   gl_Position = vec4(position * vec2(1,-1), 0,1);

绘制矩形

和昨天学习的缓冲区对象一样,创建、绑定、赋值、开启一梭哈。

   let uPosition = this.webgl.getAttribLocation(this.program!, "a_position");
   //  设置顶点

      let positionBuffer = this.webgl?.createBuffer()!;
      this.webgl?.bindBuffer(this.webgl.ARRAY_BUFFER, positionBuffer);

      this.webgl?.bufferData(
        this.webgl.ARRAY_BUFFER,
        new Float32Array(this.positionData),
        this.webgl.STATIC_DRAW
      );

      this.webgl?.enableVertexAttribArray(uPosition);
      this.webgl?.vertexAttribPointer(uPosition,2,this.webgl.FLOAT,false,0,0);

再把着色器定义的两个变量赋值。


 //   设置长宽
 this.webgl?.uniform2fv(u_resolution[this.canvas!.clientWidth,this.canvas!.clientHeight]);

 //   设置位移
 this.webgl?.uniform2fv(u_translation, [200 , 200]);

gl.uniform2fv 是给着色器uniform常量赋值的方法,其中2表示分量长度,f表示浮点数

我们拖拽会产生不同的位移数值,所以将位移设置为变量。

紧接着我们把与固定代码如初始化Shader、绘制drawArray跳过,最后会放上源码,此时我们就可以看到矩形的效果了

矩形呈现

image.png

目前来看,完成了4个需求里面的第一个,接着我们来看自转。

要想实现旋转,我们需要回到当年的数学知识:三角函数

我们来画一张图来解析一下旋转

5.png
我们要求得P旋转N°后的P1点。

通过三角函数得知: X2 = sin(b+a), Y2 = cos(b+a);

再通过和角公式可得:

X2 = sina⋅cosb+cosa⋅sinb

Y2 = cosa⋅cosb−sina⋅sinb

又得知:sin(a) = X1,cos(a) = Y1;所以最终可以得出

X2 = X1 * cos(radian) + Y1 * sin(radian)

Y2 = Y1 * cos(radian) - X1 * sin(radian)

所以我们可以写一个旋转函数:

let animate = () => {
      let radian = (Math.PI / 180) * 1;
      this.positionData = this.positionData.map((v, key) => {
        let number = 0;

        if (key % 2 === 0) {
          number =
            v * Math.cos(radian) -
            this.positionData[key + 1] * Math.sin(radian);
        } else {
          number =
            v * Math.cos(radian) +
            this.positionData[key - 1] * Math.sin(radian);
        }

        return number;
      });

      this.draw();
      requestAnimationFrame(animate);
    };

通过旋转改变顶点位置,从而达到旋转效果。

rotate.gif

至此我们就完成了第二个业务:自转。

添加纹理贴图

tips: 今天并不会详细的讲纹理方面的东西,主要还是实现功能,后面我们会有专门的文章进行学习

我们会发现在上面的顶点着色器有这两行代码

attribute vec2 a_texCoord; 
varying vec2 v_texCoord;

我们需要告诉矩形中每个顶点对应的纹理坐标,绘制点的顺序需要与顶点坐标一致。v_texCoord就是纹理坐标。

首先我们需要定义片元着色器

  static FRAGMENT_SHADER: string = `
    uniform sampler2D u_image;
    precision mediump float;
    varying vec2 v_texCoord;
    void main() {
      gl_FragColor = texture2D(u_image, v_texCoord);
    }
  `;

新增陌生全局常量:u_image = 纹理单元,texture2D 寻找纹理的颜色值方法。

紧接着我们就需要给到纹理坐标赋值。

 let image = new Image();
    image.src = "/src/assets/image/2106311D2_0.jpg";
    image.onload = () => {
      var texCoordLocation = this.webgl!.getAttribLocation(
        this.program!,
        "a_texCoord"
      );

      let texCoordBuffer = this.webgl!.createBuffer();
      this.webgl!.bindBuffer(this.webgl!.ARRAY_BUFFER, texCoordBuffer);
      this.webgl!.bufferData(
        this.webgl!.ARRAY_BUFFER,
        new Float32Array([
          0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0
        ]),
        this.webgl!.STATIC_DRAW
      );

      this.webgl!.enableVertexAttribArray(texCoordLocation);
      this.webgl!.vertexAttribPointer(
        texCoordLocation,
        2,
        this.webgl!.FLOAT,
        false,
        0,
        0
      );

      var texture = this.webgl!.createTexture();
      this.webgl!.bindTexture(this.webgl?.TEXTURE_2D!, texture);

      this.webgl!.texParameteri(this.webgl!.TEXTURE_2D,this.webgl!.TEXTURE_WRAP_S,this.webgl!.CLAMP_TO_EDGE);
      this.webgl!.texParameteri(this.webgl!.TEXTURE_2D,this.webgl!.TEXTURE_WRAP_T,this.webgl!.CLAMP_TO_EDGE);
      this.webgl!.texParameteri(this.webgl!.TEXTURE_2D,this.webgl!.TEXTURE_MIN_FILTER,this.webgl!.NEAREST);
      this.webgl!.texParameteri(this.webgl!.TEXTURE_2D,this.webgl!.TEXTURE_MAG_FILTER,this.webgl!.NEAREST);

      // 将图像上传到纹理
      this.webgl!.texImage2D(this.webgl!.TEXTURE_2D,0,this.webgl!.RGBA,this.webgl!.RGBA,this.webgl!.UNSIGNED_BYTE,image);
    };

突然来了这么一长串代码,请不要慌张。

  1. 首先我们需要等待图片加载完成之后才能操作,所以需要在image.onLoad函数中执行绑定纹理贴图。
  2. 从代码中我们可以看到老朋友缓冲区对象,将矩形提供纹理坐标。纹理坐标需与顶点绘制顺序一致。
  3. 纹理贴图步骤:createTexture(创建纹理)=》bindTexture(绑定纹理) =》texParameteri(设置纹理参数)=》(texImage2D)将图像上传到纹理。

完成加载图像纹理后效果如下:

texture.gif

至此:我们完成了第三个业务:纹理贴图。

还剩下最后一个拖拽,相信在座的童鞋们肯定不在话下,我也就不在此献丑了,献上拖拽代码:


  judgeEventInPoint(eventX, eventY) {
    let sizeX = 100;
    let sizeY = 100;

    if (
      eventX > this.translateX - sizeX &&
      eventX < this.translateX + sizeX &&
      eventY > this.translateY - sizeY &&
      eventY < this.translateY + sizeY
    ) {
      return true;
    } else {
      return false;
    }
  }
  
    addDragHandle() {
    if (this.webgl) {
      if (this.canvas) {
        this.canvas.onmousedown = downEvent => {
          let x = downEvent.clientX;
          let y = downEvent.clientY;

          let offsetLeft = this.translateX - x;
          let offsetTop = this.translateY - y;

          if (this.judgeEventInPoint(x, y)) {
            window.onmousemove = e => {
              console.log(this.translateX - x);
              this.translateX = e.clientX + offsetLeft;
              this.translateY = e.clientY + offsetTop;
            };

            window.onmouseup = () => {
              window.onmousemove = null;
              window.onmouseup = null;
            };
          }
        };
      }
    }
  }

主要逻辑是点击判断是否落在矩形上,随后拖动改变位移 u_resolution的值,从而完成拖拽功能。
效果如下:

drag.gif

好了,以上4个功能点全部完成,可以看到效果还是不错的!

最后

这一章主要还是过了一遍位移和旋转还有纹理的一些知识,可以看到旋转需要每次更改顶点位置,但其实有更好的方式,相信有人已经猜到了,那就是矩阵!敬请期待。

夜深了,写完后感觉收获颇多,很多在学习的时候含糊不清的东西,写完以后也是清晰了很多。

好了,买瓶可乐续下命,下篇文章见!

源码如下:

<template>
  <div>
    <canvas class="canvas" width="500" height="500"></canvas>
  </div>
</template>

<script setup lang="ts">
import { onMounted } from "vue";

class GL {
  static VERTEX_SHADER: string = `
     attribute vec2 a_position;
     uniform vec2 u_translation;
     uniform vec2 u_resolution;
     attribute vec2 a_texCoord;
     varying vec2 v_texCoord;

     void main() {
        vec2 position =  (a_position + u_translation) / u_resolution * 2.0 - 1.0;
        gl_Position = vec4(position * vec2(1,-1), 0,1);
        v_texCoord = a_texCoord;
     }
  `;

  static FRAGMENT_SHADER: string = `
    uniform sampler2D u_image;
    precision mediump float;
    varying vec2 v_texCoord;
    void main() {
      gl_FragColor = texture2D(u_image, v_texCoord);
    }
  `;

  public translateX: number = 200;
  public translateY: number = 200;

  public webgl: WebGLRenderingContext | null = null;
  public canvas: HTMLCanvasElement | null = null;
  public program: WebGLProgram | null = null;
  public positionData: number[] = [
    -100, -100, 100, -100, -100, 100, 100, 100, 100, -100, -100, 100
  ];

  constructor(canvas: HTMLCanvasElement) {
    this.canvas = canvas;
    this.webgl = canvas.getContext("webgl");

    this.webgl!.viewport(0, 0, canvas.clientWidth, canvas.clientHeight);
  }

  init() {
    this.addDragHandle();
    this.initShader();
    this.draw();
    this.loadTexture();

    let animate = () => {
      let radian = (Math.PI / 180) * 1;
      this.positionData = this.positionData.map((v, key) => {
        let number = 0;

        if (key % 2 === 0) {
          number =
            v * Math.cos(radian) -
            this.positionData[key + 1] * Math.sin(radian);
        } else {
          number =
            v * Math.cos(radian) +
            this.positionData[key - 1] * Math.sin(radian);
        }

        return number;
      });

      this.draw();
      requestAnimationFrame(animate);
    };

    animate();
  }

  judgeEventInPoint(eventX, eventY) {
    let sizeX = 100;
    let sizeY = 100;

    if (
      eventX > this.translateX - sizeX &&
      eventX < this.translateX + sizeX &&
      eventY > this.translateY - sizeY &&
      eventY < this.translateY + sizeY
    ) {
      return true;
    } else {
      return false;
    }
  }

  loadTexture() {
    let image = new Image();
    image.src = "/src/assets/image/2106311D2_0.jpg";
    image.onload = () => {
      var texCoordLocation = this.webgl!.getAttribLocation(
        this.program!,
        "a_texCoord"
      );

      let texCoordBuffer = this.webgl!.createBuffer();
      this.webgl!.bindBuffer(this.webgl!.ARRAY_BUFFER, texCoordBuffer);
      this.webgl!.bufferData(
        this.webgl!.ARRAY_BUFFER,
        new Float32Array([
          0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0, 1.0
        ]),
        this.webgl!.STATIC_DRAW
      );

      this.webgl!.enableVertexAttribArray(texCoordLocation);
      this.webgl!.vertexAttribPointer(
        texCoordLocation,
        2,
        this.webgl!.FLOAT,
        false,
        0,
        0
      );

      var texture = this.webgl!.createTexture();
      this.webgl!.bindTexture(this.webgl?.TEXTURE_2D!, texture);

      this.webgl!.texParameteri(
        this.webgl!.TEXTURE_2D,
        this.webgl!.TEXTURE_WRAP_S,
        this.webgl!.CLAMP_TO_EDGE
      );
      this.webgl!.texParameteri(
        this.webgl!.TEXTURE_2D,
        this.webgl!.TEXTURE_WRAP_T,
        this.webgl!.CLAMP_TO_EDGE
      );
      this.webgl!.texParameteri(
        this.webgl!.TEXTURE_2D,
        this.webgl!.TEXTURE_MIN_FILTER,
        this.webgl!.NEAREST
      );
      this.webgl!.texParameteri(
        this.webgl!.TEXTURE_2D,
        this.webgl!.TEXTURE_MAG_FILTER,
        this.webgl!.NEAREST
      );

      // 将图像上传到纹理
      this.webgl!.texImage2D(
        this.webgl!.TEXTURE_2D,
        0,
        this.webgl!.RGBA,
        this.webgl!.RGBA,
        this.webgl!.UNSIGNED_BYTE,
        image
      );
    };
  }

  addDragHandle() {
    if (this.webgl) {
      if (this.canvas) {
        this.canvas.onmousedown = downEvent => {
          let x = downEvent.clientX;
          let y = downEvent.clientY;

          let offsetLeft = this.translateX - x;
          let offsetTop = this.translateY - y;

          if (this.judgeEventInPoint(x, y)) {
            window.onmousemove = e => {
              console.log(this.translateX - x);
              this.translateX = e.clientX + offsetLeft;
              this.translateY = e.clientY + offsetTop;
            };

            window.onmouseup = () => {
              window.onmousemove = null;
              window.onmouseup = null;
            };
          }
        };
      }
    }
  }

  initShader() {
    if (this.webgl) {
      let vertexShader = this.webgl.createShader(this.webgl.VERTEX_SHADER)!;
      let fragmentShader = this.webgl.createShader(this.webgl.FRAGMENT_SHADER)!;

      this.webgl.shaderSource(vertexShader, GL.VERTEX_SHADER);
      this.webgl.shaderSource(fragmentShader, GL.FRAGMENT_SHADER);

      this.webgl.compileShader(vertexShader);
      this.webgl.compileShader(fragmentShader);

      let program = this.webgl.createProgram()!;
      this.webgl.attachShader(program, vertexShader);
      this.webgl.attachShader(program, fragmentShader);

      if (
        !this.webgl.getShaderParameter(vertexShader, this.webgl.COMPILE_STATUS)
      ) {
        console.log(this.webgl.getShaderInfoLog(vertexShader));
      }

      this.webgl.linkProgram(program);
      this.webgl.useProgram(program);

      if (!this.webgl.getProgramParameter(program, this.webgl.LINK_STATUS)) {
        var info = this.webgl.getProgramInfoLog(program);
        console.log(info);
      }

      this.program = program;
    }
  }

  draw() {
    if (this.webgl) {
      let uPosition = this.webgl.getAttribLocation(this.program!, "a_position");
      let u_resolution = this.webgl?.getUniformLocation(
        this.program!,
        "u_resolution"
      );

      let u_translation = this.webgl?.getUniformLocation(
        this.program!,
        "u_translation"
      );

      //  设置顶点

      let positionBuffer = this.webgl?.createBuffer()!;
      this.webgl?.bindBuffer(this.webgl.ARRAY_BUFFER, positionBuffer);

      this.webgl?.bufferData(
        this.webgl.ARRAY_BUFFER,
        new Float32Array(this.positionData),
        this.webgl.STATIC_DRAW
      );

      this.webgl?.enableVertexAttribArray(uPosition);

      this.webgl?.vertexAttribPointer(
        uPosition,
        2,
        this.webgl.FLOAT,
        false,
        0,
        0
      );

      //   设置长宽

      this.webgl?.uniform2fv(u_resolution, [
        this.canvas!.clientWidth,
        this.canvas!.clientHeight
      ]);

      //   设置位移

      this.webgl?.uniform2fv(u_translation, [this.translateX, this.translateY]);

      //   绘制

      this.webgl?.clearColor(0.0, 0.0, 0.0, 1.0);
      this.webgl?.clear(
        this.webgl.COLOR_BUFFER_BIT | this.webgl.DEPTH_BUFFER_BIT
      );

      this.webgl?.drawArrays(
        this.webgl.TRIANGLES,
        0,
        new Float32Array(this.positionData).length / 2
      );
    }
  }
}

onMounted(() => {
  let gl = new GL(document.querySelector(".canvas") as HTMLCanvasElement);
  gl.init();
});
</script>

<style scoped></style>

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值