likes
comments
collection
share

作为前端的你需要学习WebGL啦 (保姆级教程二)

作者站长头像
站长
· 阅读数 62

OpenGL ES

基本特点

  • 大小写敏感

  • 强制分号

  • 着色器语言通过main函数作为入口,没有任何返回值。

  • 注释同js注释相同。

  • 强制类型语言

    • float 单精度浮点类型
    • bool
    • int
  • 变量命名不能以 gl_webgl__webgl_ 作为开头。

  • 数据类型转换

float()
bool()
int()
// 可以将对应的变量转换为其他的类型。
  • 可以使用分支和循环语句。

    • break, continue可以跳出循环。
    • discard 只能在片元着色器中使用,表示放弃当前片元直接处理下一个片元。
  • 可以定义函数

    // 返回值类型 函数名 (参数) { return 返回值}
     void main() {
      gl_Position = aPosition; // vec4(0.0,0.0,0.0,1.0)
      vTex = vec2(aTex.x, aTex.y);
    }

矢量类型

参考我们前面定义的顶点坐标类型。

  • vec2、vec3、vec4 具有 2,3,4 个浮点数元素的矢量。
  • ivec2、ivec3、ivec4 具有 2,3,4 个整型元素的矢量。
  • bvec2、bvec3、bvec4 具有 2,3,4 个布尔值元素的矢量。

我们可以通过vec4构造函数为矢量类型赋值。

   const FRAGMENT_SHADER_SOURCE = `
    void main() {
      gl_FragColor = vec4(1.0,0.0,0.0,1.0);
    }
  `; // 片元着色器

访问矢量里的分量。

  • x, y, z, w 访问顶点坐标的分量
  const VERTEX_SHADER_SOURCE = `
    attribute vec4 aPosition;
    attribute float aTranslate;
    void main() {
      gl_Position = vec4(aPosition.x + aTranslate, aPosition.y, aPosition.z, 1.0);
      gl_PointSize = 10.0;
    }
  `; // 顶点着色器

作为前端的你需要学习WebGL啦 (保姆级教程二) 并且可以通过混合的方式获取多个值,获取到的是⼀个新的矢量内容。

作为前端的你需要学习WebGL啦 (保姆级教程二)

  • s, t, p, q 访问纹理坐标分量。

矩阵类型

mat2、mat3、mat4 分别为2 * 2, 3 * 3, 4 * 4 的浮点数元素矩阵。矩阵参数是列主序的。

  // 创建着色器源码
  const VERTEX_SHADER_SOURCE = `
    attribute vec4 aPosition;
    uniform mat4 mat; // 平移时需要平移三角形的所有顶点,所以使用uniform属性
    void main() {
      gl_Position = mat * aPosition;
      gl_PointSize = 10.0;
    }
  `; // 顶点着色器

纹理取样器类型

他们都只能通过uniform声明。

  • sampler2D,2D取样器。
   const FRAGMENT_SHADER_SOURCE = `
    precision lowp float; // 定义精度
    uniform sampler2D uSampler;  // 定义纹理取样器
    varying vec2 vTex;

    void main() {
      // 从图像中逐片元获取内容进行颜色填充
      gl_FragColor = texture2D(uSampler, vTex);
    }
  `; // 片元着色器
  • samplerCube,3D取样器

限定词

  • const,声明一个常量,定义之后不能被改变。
  • attribute只能出现在顶点着色器中,只能声明为全局变量,表示逐顶点信息。单个顶点的信息。
  • uniform, 可同时出现在 顶点着色器 和 片元着⾊器中。只读类型,强调一致性。用来存储的是影响所有顶点的数据。 如变换矩阵。
  • varying, 从顶点着色器向片元着色器传递数据。
  // 创建着色器源码
  const VERTEX_SHADER_SOURCE = `
    attribute vec4 aPosition;

    varying vec4 vColor; // 主要作用,顶点着色器向片元着色器中传递数据

    void main() {
      vColor = aPosition;

      gl_Position = aPosition;
    }
  `; // 顶点着色器

  const FRAGMENT_SHADER_SOURCE = `
    precision lowp float; // 片元着色器中需要声明精度
    varying vec4 vColor; // 这里需要和顶点着色器声明一样的内容

    void main() {
      gl_FragColor = vColor;
    }
  `; // 片元着色器

精度限定

作用是提升运行效率,削减内存开⽀。

  • 单独对于一个变量限制精度。mediump float f;
  • 通过 precision 关键字来修改着色器的默认精度。
  const FRAGMENT_SHADER_SOURCE=`
    // ⾼精度:highp, 低精度:lowp,
    precision mediump float; // 必须设置精度
    // 注意这里没有vec1类型,需要使用float代替
    uniform float uColor; // 这里定义的vec和下面的赋值需要对应。
    void main() {
      gl_FragColor = vec4(uColor, 0.0, 0.0,1.0); // vec4
    }
  `; // 片元着色器

缓存区对象

缓冲区对象是WebGL系统中的一块内存区域,可以一次性地向缓冲区对象中填充大量的顶点数据,然后将这些数据保存在其中,供顶点着色器使用。

类型化数组

由于缓冲区对象存取的是大量相同类型数据,所以我们就可以使用类型化数组提供数据。

在 webgl 中,需要处理⼤量的相同类型数据,所以引入类型化数组,这样程序就可以预知到数组中的数据类型, 提⾼性能。

作为前端的你需要学习WebGL啦 (保姆级教程二)

作为前端的你需要学习WebGL啦 (保姆级教程二)

  const points = new Float32Array([
    -0.5, -0.5,
     0.5, -0.5,
     0.0,  0.5,
  ])

创建一块内存区域

  const buffer = gl.createBuffer();

将缓冲区绑定到gl上

   const buffer = gl.createBuffer();
  /**
   * gl.bindBuffer(target, buffer)
   * buffer: 已经创建好的缓冲区对象
   * target:填入的是数据还是索引
   * - gl.ARRAY_BUFFER: 表示缓冲区存储的是顶点的数据
   * - gl.ELEMENT_ARRAY_BUFFER: 表示缓冲区存储的是顶点的索引值
   * 
   * */
  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

  /**
   * gl.bufferData(target, data, type)
   * target:和bindBuffer的target一样的类型
   * data:写⼊缓冲区的顶点数据。
   * type: 如何写入数据,表示如何使⽤缓冲区对象中的数据
   * - gl.STATIC_DRAW: 写⼊⼀次,多次绘制
   * - gl.STREAM_DRAW: 写⼊⼀次,绘制若干次
   * - gl.DYNAMIC_DRAW: 写⼊多次,绘制多次
  */
  gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);
  /***
   * gl,vertexAttribPointer(location, size, type, normalized, stride, offset)
   * 将缓冲区数据复制到顶点属性上。
   * location:attribute变量位置
   * size:指数量定每个顶点属性的组成
   * type:指定数组中每个元素的数据类型 
   *  - gl.FLOAT:浮点型
   *  - gl.UNSIGNED_BYTE:⽆符号字节 
   *  - gl.SHORT:短整型
   *  - gl.UNSIGNED_SHORT:无符号短整型
   *  - gl.INT:整型
   *  - gl.UNSIGNED_INT:无符号整型
   * normalized:当转换为浮点数时是否应该将整数数值归一化到特定的范围(是否转化为-1,1之间)
   * stride:两个相邻顶点之间的字节数。可通过points.BYTES_PER_ELEMENT获取,这个在多个属性公用一个缓冲区时有用。
   * offset:数据偏移量字节数。就是每个顶点在缓冲区取数据是否是连续的,不连续则需要指定偏移量
   * */
  gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);

  // 激活attribute属性
  gl.enableVertexAttribArray(aPosition)
  // gl.vertexAttrib2f(aPosition, 0.0, 0.0)
  /***
   * 绘制
   * 参数二:从哪个顶点开始绘制
   * 参数三:绘制几个顶点
   * */
  gl.drawArrays(gl.POINTS, 0, 3);

作为前端的你需要学习WebGL啦 (保姆级教程二)

缓冲区执行流程

作为前端的你需要学习WebGL啦 (保姆级教程二)

多个属性共用一个缓冲区

通过gl,vertexAttribPointer(location, size, type, normalized, stride, offset)中的tride, offset实现。

const ctx = document.getElementById('canvas')

  const gl = ctx.getContext('webgl')

  // 创建着色器源码
  const VERTEX_SHADER_SOURCE = `
    attribute vec4 aPosition;
    attribute float aPointSize;
    void main() {
      gl_Position = aPosition;
      gl_PointSize = aPointSize;
    }
  `; // 顶点着色器

  const FRAGMENT_SHADER_SOURCE = `
    void main() {
      gl_FragColor = vec4(1.0,0.0,0.0,1.0);
    }
  `; // 片元着色器

  const program = initShader(gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE)

  const aPosition = gl.getAttribLocation(program, 'aPosition');
  const aPointSize = gl.getAttribLocation(program, 'aPointSize');

  const points = new Float32Array([
    -0.3, -0.3, 10.0, // 10.0
    0.3, -0.3, 30.0, // 30.0
    0.3,  0.3, 50.0, // 50.0
  ])

  const buffer = gl.createBuffer();

  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

  gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);

  // 获取1个数据字节数
  const BYTES = points.BYTES_PER_ELEMENT;
  // 每个顶点有三个数据组成,所以偏移三个BYTES,其实就是一个顶点多少个字节数
  gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, BYTES * 3, 0);
  // 激活aPosition attribute属性
  gl.enableVertexAttribArray(aPosition)
  // 应为3个数据为一组,一组中最后一个数字为大小数据,所以数据偏移量为BYTES * 2
  gl.vertexAttribPointer(aPointSize, 1, gl.FLOAT, false, BYTES * 3, BYTES * 2);
  // 激活aPointSize attribute属性
  gl.enableVertexAttribArray(aPointSize)

  gl.drawArrays(gl.POINTS, 0, 3);

作为前端的你需要学习WebGL啦 (保姆级教程二)

绘制其他图形

使用drawArrays来绘制图形。

  • gl.POINTS 点 一系列点
  • gl.LINES 线段 一系列单独的线段,如果顶点是奇数,最后一个会被忽略。
  • gl.LINE_LOOP 闭合线 一系列连接的线段,结束时,会闭合终点和起点
  • gl.LINE_STRIP 线条 一系列连接的线段,不会闭合终点和起点
  • gl.TRIANGLES 三角形 一系列单独的三角形,如果想绘制多个三角形,我们必须传入的顶点数为3的倍数。
  • gl.TRIANGLE_STRIP 三角带 一系列条带状的三角形
  • gl.TRIANGLE_FAN 三角形 飘带状三角形

纹理绘制

作为前端的你需要学习WebGL啦 (保姆级教程二)


  const ctx = document.getElementById('canvas')

  const gl = ctx.getContext('webgl')

  // 创建着色器源码
  const VERTEX_SHADER_SOURCE = `
    // 只传递顶点数据
    attribute vec4 aPosition;
    // 声明纹理坐标
    attribute vec4 aTex;
    // texture2D接收vec2类型,所以这里定义vec2类型
    varying vec2 vTex;

    void main() {
      gl_Position = aPosition; // vec4(0.0,0.0,0.0,1.0)
      vTex = vec2(aTex.x, aTex.y);
    }
  `; // 顶点着色器

  const FRAGMENT_SHADER_SOURCE = `
    precision lowp float; // 定义精度
    uniform sampler2D uSampler;  // 定义纹理取样器
    varying vec2 vTex;

    void main() {
      
      /**
       * 从图像中逐片元获取内容进行颜色填充
       * vec4 texture2D(sampler2D sampler, vec2 coord)
       * 
       * sampler 纹理单元编号
       * 
       * coord 纹理坐标 (图片坐标)
       * 
       * **/
      gl_FragColor = texture2D(uSampler, vTex);
    }
  `; // 片元着色器

  const program = initShader(gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE)

  const aPosition = gl.getAttribLocation(program, 'aPosition');
  const aTex = gl.getAttribLocation(program, 'aTex');
  const uSampler = gl.getUniformLocation(program, 'uSampler');

  const points = new Float32Array([
    -0.5,  0.5, 0.0, 1.0,
    -0.5, -0.5, 0.0, 0.0,
     0.5,  0.5, 1.0, 1.0,
     0.5, -0.5, 1.0, 0.0,
  ])

  const buffer = gl.createBuffer();
  const BYTES = points.BYTES_PER_ELEMENT;

  gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

  gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW);

  // 每个顶点相差是个字节数据
  gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, BYTES * 4, 0);

  gl.enableVertexAttribArray(aPosition)
  // 顶点数据的偏移量,每组数据我们的前两个才是顶点需要的数据
  gl.vertexAttribPointer(aTex, 2, gl.FLOAT, false, BYTES * 4, BYTES * 2);

  gl.enableVertexAttribArray(aTex)

  const img = new Image();
  img.onload = function() {
    // 创建纹理对象,存储纹理数据
    const texture = gl.createTexture();

    // 翻转 图片 Y轴 ,因为他和webgl的坐标方向不一样。
    gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)

    // 开启一个纹理单元
    // Webgl 是通过纹理单元来管理纹理对象,每个纹理单元管理⼀张纹理图像。
    gl.activeTexture(gl.TEXTURE0);

    /**
     * 绑定纹理对象
     * gl.bindTexture(type, texture)
     * - type 参数有以下两种:
     *  - gl.TEXTURE_2D: 二维纹理 
     *  - gl.TEXTURE_CUBE_MAP: 立方体纹理
     * - texture 纹理对象
     * **/
    
    gl.bindTexture(gl.TEXTURE_2D, texture);

    /**
     * 图像变化的时候获取纹理信息
     * gl.texParamteri(type, pname, param)
     * 
     * type 同上
     * 
     * pname
     * - gl.TEXTURE_MAG_FILTER 放⼤ 
     * - gl.TEXTURE_MIN_FILTER 缩⼩
     * - gl.TEXTURE_WRAP_S 横向(⽔平填充)
     * - gl.TEXTURE_WRAP_T 纵向(垂直填充) 
     * 
     * 当pname为gl.TEXTURE_MAG_FILTER和 gl.TEXTURE_MIN_FILTER param 可以为
     * - gl.NEAREST 使用像素颜色值
     * - gl.LINEAR 使用四周的加权平均值
     * 
     * 当pname为gl.TEXTURE_WRAP_S和 gl.TEXTURE_WRAP_T param 可以为
     * - gl.REPEAT 平铺重复
     * - gl.MIRRORED_REPEAT 镜像对称
     * - gl.CLAMP_TO_EDGE 边缘延伸
     * **/
    // 处理放大缩小的逻辑
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)
    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)

    // 横向 纵向 平铺的方式
    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(type, level, internalformat, format,dataType, image)
     * 
     * type 同上
     * 
     * level 为0
     * 
     * internalformat 图像的内部格式
     * - gl.RGB
     * - gl.RGBA
     * - gl.ALPHA
     * - gl.LUMINANCE 使用物体表面的 红绿蓝 分量的加权平均值来计算
     * - gl.LUMINANCE_ALPHA
     * 
     * format 纹理的内部格式,必须和 internalformat 相同
     * 
     * dataType 纹理数据的数据类型
     * - gl.UNSIGNED_BYTE 
     * - gl.UNSIGNED_SHORT_5_6_5
     * - gl.UNSIGNED_SHORT_4_4_4_4 
     * - gl.UNSIGNED_SHORT_5_5_5_1 
     * 
     * image 图片对象
     * **/
    
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, img);

    // 为uniform变量赋值,赋值纹理单元(编号)
    gl.uniform1i(uSampler, 0);

    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  }

  img.src = '../assets/wenlitest.jpg'

作为前端的你需要学习WebGL啦 (保姆级教程二) 作为前端的你需要学习WebGL啦 (保姆级教程二)

3D基础

辅助函数

  • 归一化函数, 归一化到 0-1 的区间内
// 归一化函数
function normalized(arr) {
  let sum = 0;

  for (let i = 0; i < arr.length; i++) {
    sum += arr[i] * arr[i]
  }

  const middle = Math.sqrt(sum);

  for (let i = 0; i < arr.length; i++) {
    arr[i] = arr[i] / middle;
  }
}
  • 叉积,求两个平面的法向量

// 叉积函数 获取法向量
function cross(a,b) {
  return new Float32Array([
    a[1] * b[2] - a[2] * b[1],
    a[2] * b[0] - a[0] * b[2],
    a[0] * b[1] - a[1] * b[0],
  ])
}
  • 点积,求某点在x,y,z轴上的投影长度

// 点积函数 获取投影长度
function dot(a, b) {
  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}


  • 向量差,获取视点到目标点之间的向量
// 向量差
function minus(a, b) {
  return new Float32Array([
    a[0] - b[0],
    a[1] - b[1],
    a[2] - b[2],
  ])
}
  • 获取视图矩阵
// 视图矩阵获取
function getViewMatrix(eyex, eyey, eyez, lookAtx, lookAty, lookAtz, upx, upy, upz) {

  // 视点
  const eye = new Float32Array([eyex, eyey, eyez])
  // 目标点
  const lookAt = new Float32Array([lookAtx, lookAty, lookAtz])
  // 上方向
  const up = new Float32Array([upx, upy, upz])

  // 确定z轴
  const z = minus(eye, lookAt);

  normalized(z);
  normalized(up);

  // 确定x轴
  const x = cross(z, up);

  normalized(x);
  // 确定y轴
  const y = cross(x, z);

  return new Float32Array([
    x[0],       y[0],       z[0],       0,
    x[1],       y[1],       z[1],       0,
    x[2],       y[2],       z[2],       0,
    -dot(x,eye),-dot(y,eye),-dot(z,eye),1
  ])
}

正射投影

空间中的物体投影到平面上的尺寸是一样的。

作为前端的你需要学习WebGL啦 (保姆级教程二)

获取正射投影矩阵

// 获取正射投影矩阵
function getOrtho(l, r, t, b, n, f) { // 左右上下近远
  return new Float32Array([
    2 / (r - l), 0,           0,           0,
    0,           2/(t-b),     0,           0,
    0,           0,           -2/(f-n),    0,
    -(r+l)/(r-l),-(t+b)/(t-b),-(f+n)/(f-n),1
  ])
}

透视投影

能够反映物体的空间形象。

// 获取透视投影矩阵
function getPerspective(fov, aspect, far, near) {
  fov = fov * Math.PI / 180;
  return new Float32Array([
    1/(aspect*Math.tan(fov / 2)), 0, 0, 0,
    0, 1/(Math.tan(fov/2)),0,0,
    0,0,-(far+near)/(far-near),-(2*far*near)/(far-near),
    0,0,-1,0,
  ])
}

往期文章

转载自:https://juejin.cn/post/7270416462677590068
评论
请登录